{"_id":"@1meeting/auth-client-sdk","_rev":"2-5a86d9e561a9cc0ea6879e5cbbb13584","name":"@1meeting/auth-client-sdk","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.1":{"name":"@1meeting/auth-client-sdk","version":"0.1.1","license":"MIT","_id":"@1meeting/auth-client-sdk@0.1.1","maintainers":[{"name":"1meeting","email":"liuwei@1meeting.com"}],"dist":{"shasum":"08c40e8e8c8d3d311fa78ca1b465cc2b00b5a277","tarball":"https://registry.npmjs.org/@1meeting/auth-client-sdk/-/auth-client-sdk-0.1.1.tgz","fileCount":18,"integrity":"sha512-7pNd6eYWdOYj/0p6Gp/gm+HR4fcf8DupiovGW9DbC/sf+GVqusYWh1HiVLKOApixOUEO0pPqbRZ+pkFcOV8MGA==","signatures":[{"sig":"MEUCICBTi7ruH2jLFbz0kdvs6lg4/1kBW6aBeqJjoWdVlBy5AiEAwigqXQcvZSFrCgo2K03dn1sSZe6qNAJdv4KYRvZxVOo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":17883},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./browser":{"types":"./dist/browser.d.ts","import":"./dist/browser.js"}},"gitHead":"9f56bebf21de3cc9135749c4c91fdfaaffca16b5","private":false,"scripts":{"build":"tsc -p tsconfig.json","prepublishOnly":"npm run build"},"_npmUser":{"name":"1meeting","email":"liuwei@1meeting.com"},"_npmVersion":"11.11.0","description":"HighGood 认证中心 OAuth2 + PKCE 客户端：Node 机密客户端与浏览器公钥（PKCE）","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.8.3","@types/node":"^22.14.1"},"_npmOperationalInternal":{"tmp":"tmp/auth-client-sdk_0.1.1_1777113584956_0.6503608124989415","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@1meeting/auth-client-sdk","version":"0.2.0","private":false,"description":"HighGood 认证中心 OAuth2 + PKCE 客户端：Node 机密客户端与浏览器公钥（PKCE）","license":"MIT","type":"module","sideEffects":false,"main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./browser":{"types":"./dist/browser.d.ts","import":"./dist/browser.js"}},"scripts":{"build":"tsc -p tsconfig.json","test":"npm run build && node --test test/*.test.mjs","prepublishOnly":"npm run build"},"devDependencies":{"@types/node":"^22.14.1","typescript":"^5.8.3"},"engines":{"node":">=18"},"publishConfig":{"registry":"https://registry.npmjs.org/","access":"public"},"gitHead":"7da81941b8ea9f83cdd8ab1334c2cc61f707aa0f","_id":"@1meeting/auth-client-sdk@0.2.0","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-ro7uud6eOkBEeLjhdjQz+JHDF4Dmj9/wnA1xRYnIXSA7BXPGXLf5WljxuM9urA4tasQ7XPtu5+j/KSfDz9bN2A==","shasum":"d73189afcbf25e008c82e2491ff4c3904ab9c4d8","tarball":"https://registry.npmjs.org/@1meeting/auth-client-sdk/-/auth-client-sdk-0.2.0.tgz","fileCount":18,"unpackedSize":26470,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIG5J9sml3K3dkTGhDgHdcF/XeAuFcKYrmvb0ETFQDd1vAiAyyP3vVEecthHJPX/Djst4YrKWvZXgKmFRJq52tw4xiQ=="}]},"_npmUser":{"name":"1meeting","email":"liuwei@1meeting.com"},"directories":{},"maintainers":[{"name":"1meeting","email":"liuwei@1meeting.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/auth-client-sdk_0.2.0_1777420560610_0.029923849861357432"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-25T10:39:44.894Z","modified":"2026-04-28T23:56:00.885Z","0.1.1":"2026-04-25T10:39:45.105Z","0.2.0":"2026-04-28T23:56:00.774Z"},"license":"MIT","description":"HighGood 认证中心 OAuth2 + PKCE 客户端：Node 机密客户端与浏览器公钥（PKCE）","maintainers":[{"name":"1meeting","email":"liuwei@1meeting.com"}],"readme":"# @1meeting/auth-client-sdk\n\n面向业务应用（A/B/未来扩展应用）的统一登录接入 SDK。  \n封装了平台仓 OAuth2 授权码 + PKCE 常用能力，帮助应用快速完成“跳转授权 -> 回调换码 -> 拉取用户信息”闭环。\n\n快速上手请先看：`QUICKSTART.md`。\n\n**仓库内体系化集成说明（推荐）**：[../../docs/05-client-sdk.md](../../docs/05-client-sdk.md)（流程、环境、`authApiRoots`、双入口、安全清单）。  \n全仓文档索引：[../../docs/README.md](../../docs/README.md)。\n\n---\n\n## 1. 适用场景\n\n- 子应用不自建账号体系，接入平台仓统一登录\n- OAuth2 授权码模式（`response_type=code`）\n- 支持 Node 后端（推荐）和浏览器 PKCE 场景\n\n---\n\n## 2. 安装\n\n### 2.1 从 npmjs 安装（推荐）\n\n```bash\nnpm i @1meeting/auth-client-sdk\n```\n\n> 公开包可直接从 npmjs 安装，无需 GitHub 私有 registry token。\n\n### 2.2 本地源码构建（本仓）\n\n```bash\nnpm run build --prefix packages/auth-client-sdk\n```\n\n---\n\n## 3. 导出接口总览\n\n### Node 入口：`@1meeting/auth-client-sdk`\n\n- `buildOAuth2AuthorizeUrl(input)`（可选 `scope`）\n- `buildOAuth2LogoutRedirectUrl(input)`\n- `createNodeCodeVerifier()`\n- `createS256CodeChallenge(verifier)`\n- `createNodePkcePair()`\n- `exchangeAuthorizationCode(input)`（可选 `signal`）\n- `exchangeRefreshTokenWithRoots(input)`\n- `fetchUserInfoWithRoots(input)`（可选 `signal`）\n- `pickAccessTokenFromTokenResponse` / `pickRefreshTokenFromTokenResponse` / `pickExpiresInFromTokenResponse` / `pickTokenTypeFromTokenResponse`\n- `parseTokenFromResponse(payload)`、`getApiErrorFromPayload(payload)`\n- `pickUserInfoFromUserinfoResponse(payload)`\n- 别名兼容：\n  - `createCodeVerifier`（同 `createNodeCodeVerifier`）\n  - `createCodeChallenge`（同 `createS256CodeChallenge`）\n\n### 浏览器入口：`@1meeting/auth-client-sdk/browser`\n\n- 与 Node 入口对齐的 URL / 换码 / userinfo / 解析辅助函数（同上）\n- `createBrowserCodeVerifier()`、`createS256CodeChallenge(verifier)`（异步）、`createBrowserPkcePair()`（异步）\n- `exchangeAuthorizationCodePublic(input)`（无 `clientSecret` 的换码封装）\n\n---\n\n## 4. 标准接入流程（推荐：后端换码）\n\n1. 服务端生成 `state` + PKCE（`codeVerifier/codeChallenge`）\n2. 组装授权地址并跳转平台仓 `/api/oauth2/authorize`\n3. 回调拿到 `code` 后，服务端调用 `exchangeAuthorizationCode`\n4. 从 token 响应中解析 `accessToken`\n5. 调 `fetchUserInfoWithRoots` 获取用户信息并建立本地会话\n\n---\n\n## 5. Node 接入示例（Express/BFF）\n\n```ts\nimport crypto from 'node:crypto'\nimport {\n  createNodePkcePair,\n  buildOAuth2AuthorizeUrl,\n  exchangeAuthorizationCode,\n  fetchUserInfoWithRoots,\n  getApiErrorFromPayload,\n  parseTokenFromResponse,\n  pickAccessTokenFromTokenResponse,\n  pickUserInfoFromUserinfoResponse,\n} from '@1meeting/auth-client-sdk'\n\n// Step 1: 生成授权地址\nconst { codeVerifier, codeChallenge } = createNodePkcePair()\nconst state = crypto.randomUUID()\n\n// 你需要把 codeVerifier 按 state 暂存到 session/redis\n// saveState(state, { codeVerifier })\n\nconst authorizeUrl = buildOAuth2AuthorizeUrl({\n  authPublicBaseUrl: 'https://www.highgood.com/auth',\n  clientId: 'client_app_a',\n  redirectUri: 'https://www.highgood.com/a/api/auth/callback',\n  state,\n  nonce: crypto.randomUUID(),\n  codeChallenge,\n  codeChallengeMethod: 'S256',\n})\n\n// Step 2: 回调换码\nconst tokenResult = await exchangeAuthorizationCode({\n  code, // callback query.code\n  codeVerifier, // 从你保存的 state 中取回\n  redirectUri: 'https://www.highgood.com/a/api/auth/callback',\n  clientId: 'client_app_a',\n  clientSecret: 'app-a-secret',\n  authApiRoots: [\n    'http://auth-api.internal:3001', // 首选内网\n    'https://www.highgood.com/auth', // 可选 fallback\n  ],\n})\n\nif (!tokenResult.ok) {\n  const err = getApiErrorFromPayload(tokenResult.payload)\n  throw new Error(err?.message || `token exchange failed: ${tokenResult.status}`)\n}\n\nconst token = parseTokenFromResponse(tokenResult.payload)\nconst accessToken = token?.accessToken ?? pickAccessTokenFromTokenResponse(tokenResult.payload)\nif (!accessToken) {\n  throw new Error('token payload missing accessToken')\n}\n\n// Step 3: 获取用户信息\nconst userInfoResult = await fetchUserInfoWithRoots({\n  accessToken,\n  authApiRoots: ['http://auth-api.internal:3001'],\n})\n\nif (!userInfoResult.ok) {\n  throw new Error(`userinfo failed: ${userInfoResult.status}`)\n}\n\nconst user = pickUserInfoFromUserinfoResponse(userInfoResult.payload)\nif (!user?.sub) {\n  throw new Error('userinfo payload invalid')\n}\n```\n\n---\n\n## 6. 浏览器 PKCE 示例（纯前端）\n\n```ts\nimport {\n  createBrowserPkcePair,\n  buildOAuth2AuthorizeUrl,\n  exchangeAuthorizationCodePublic,\n} from '@1meeting/auth-client-sdk/browser'\n\nconst { codeVerifier, codeChallenge } = await createBrowserPkcePair()\nconst state = crypto.randomUUID()\nsessionStorage.setItem(`pkce:${state}`, codeVerifier)\n\nconst authorizeUrl = buildOAuth2AuthorizeUrl({\n  authPublicBaseUrl: 'https://www.highgood.com/auth',\n  clientId: 'client_app_a',\n  redirectUri: 'https://www.highgood.com/a/callback',\n  state,\n  nonce: crypto.randomUUID(),\n  codeChallenge,\n  codeChallengeMethod: 'S256',\n})\n\nlocation.href = authorizeUrl\n\n// 回调页中：\nconst code = new URL(location.href).searchParams.get('code')!\nconst callbackState = new URL(location.href).searchParams.get('state')!\nconst verifier = sessionStorage.getItem(`pkce:${callbackState}`)!\n\nconst tokenResult = await exchangeAuthorizationCodePublic({\n  code,\n  codeVerifier: verifier,\n  redirectUri: 'https://www.highgood.com/a/callback',\n  clientId: 'client_app_a',\n  authApiRoots: ['https://www.highgood.com/auth'],\n})\n```\n\n> 安全建议：生产优先用后端换码（BFF），不要在浏览器暴露机密 `clientSecret`。\n\n---\n\n## 7. API 详细说明\n\n### 7.1 `buildOAuth2AuthorizeUrl(input)`\n\n构建平台仓授权地址，返回完整 URL 字符串。\n\n**必填参数：**\n\n- `authPublicBaseUrl`：认证中心对浏览器公开入口（如 `https://www.highgood.com/auth`）\n- `clientId`\n- `redirectUri`\n- `state`\n- `nonce`\n- `codeChallenge`\n- `codeChallengeMethod`：固定 `S256`\n\n**可选参数：**\n\n- `responseType`：默认 `code`\n- `scope`：空格分隔（如 `openid profile`）；与 `extraQuery.scope` 同时存在时 **以 `scope` 为准**\n- `extraQuery`：附加 query 参数\n\n---\n\n### 7.2 `createNodePkcePair()` / `createBrowserPkcePair()`\n\n生成 `{ codeVerifier, codeChallenge }`，用于 PKCE 流程。\n\n- Node 版同步\n- Browser 版异步（WebCrypto）\n\n---\n\n### 7.3 `exchangeAuthorizationCode(input)`\n\n调用认证中心 `/api/oauth2/token` 执行换码，返回：\n\n```ts\ntype HttpResult = {\n  ok: boolean\n  status: number\n  payload: unknown\n}\n```\n\n**特点：**\n\n- `authApiRoots` 支持多地址顺序尝试\n- **仅当** `fetch` **抛网络级异常**时才尝试下一个 root\n- 一旦拿到 **任意 HTTP 响应**（含 4xx/5xx）即返回（不重复消费授权码；也不为 5xx 自动换根以免语义混乱）\n- 可选 `signal`：`AbortController` 超时/取消\n\n---\n\n### 7.4 `exchangeRefreshTokenWithRoots(input)`\n\n`POST /api/oauth2/token`，JSON：`grantType: 'refresh_token'`、`refreshToken`、`clientId`、`clientSecret`。  \n多根与 `signal` 行为同 `exchangeAuthorizationCode`。\n\n---\n\n### 7.5 `fetchUserInfoWithRoots(input)`\n\n调用认证中心 `/api/oauth2/userinfo` 获取用户信息，返回 `HttpResult`（同上）。支持 `signal`。\n\n---\n\n### 7.6 `buildOAuth2LogoutRedirectUrl(input)`\n\n拼浏览器顶层导航用的 `GET …/api/oauth2/logout-redirect?return_to=…&global=1`（`global` 可选）。\n\n---\n\n### 7.7 `parseTokenFromResponse` / `pick*` / `getApiErrorFromPayload`\n\n- `parseTokenFromResponse`：成功包络下返回 `TokenData`（`accessToken`、`refreshToken`、`tokenType`、`expiresIn`），否则 `null`\n- `pickRefreshTokenFromTokenResponse` 等：按需取单字段\n- `getApiErrorFromPayload`：从失败包络取 `code` / `message` / `traceId`；`code === 'OK'` 时返回 `null`\n\n---\n\n### 7.8 `pickUserInfoFromUserinfoResponse(payload)`\n\n从统一包络提取用户信息：\n\n- 若 `payload.code === 'OK'` 且存在 `data.sub`，返回 `UserInfoData`\n- 否则返回 `undefined`\n\n---\n\n## 8. 类型定义（核心）\n\n- `HighgoodApiEnvelope<T>`\n- `TokenData`\n- `UserInfoData`\n- `HttpResult`\n- `BuildAuthorizeUrlInput`\n- `BuildLogoutRedirectUrlInput`\n- `ExchangeCodeInput`\n- `ExchangeRefreshTokenInput`\n- `FetchUserInfoInput`\n\n详见：`src/types.ts`\n\n---\n\n## 9. 常见问题\n\n### Q1: `ERR_CONNECTION_REFUSED` / token 请求失败\n\n- 先检查 `authApiRoots` 是否可达（内网地址优先）\n- 检查平台仓服务是否已启动\n\n### Q2: 回调后提示 `state` 不合法\n\n- 确认 `state -> codeVerifier` 保存与回调读取是同一份存储（session/redis）\n- 确认 state 未过期（建议 10~15 分钟 TTL）\n\n### Q3: `redirect_uri` 不匹配\n\n- 确认 SDK 传入 `redirectUri` 与平台注册 OAuth 客户端配置完全一致（协议/域名/路径）\n\n### Q4: 浏览器端换码失败\n\n- 检查是否错误使用了机密客户端\n- 建议改为后端（BFF）换码\n\n### Q5: 换码 / userinfo 请求挂起\n\n- 为 `exchangeAuthorizationCode` / `fetchUserInfoWithRoots` / `exchangeRefreshTokenWithRoots` 传入 `signal`（例如 `AbortSignal.timeout(8000)`，按运行时支持情况 polyfill）\n\n---\n\n## 10. 发布建议\n\n- 版本号遵循 semver（`0.1.x` 修复，`0.2.x` 新能力）；变更见 `CHANGELOG.md`\n- 发布前至少验证：\n  - 授权跳转\n  - 回调换码\n  - userinfo\n  - 本地 loopback（localhost/127.0.0.1）场景\n\n","readmeFilename":"README.md"}