{"_id":"@aitianyu.cn/idp-node-sdk","_rev":"6-bb8f927529f4d5938bd4a6f50a39a101","name":"@aitianyu.cn/idp-node-sdk","dist-tags":{"latest":"4.2.0"},"versions":{"4.0.0":{"name":"@aitianyu.cn/idp-node-sdk","version":"4.0.0","keywords":["oauth2","oidc","authentication","tianyu","idp"],"license":"MIT","_id":"@aitianyu.cn/idp-node-sdk@4.0.0","maintainers":[{"name":"aitianyu.cn","email":"product@aitianyu.cn"}],"dist":{"shasum":"c996b803f43dcf52fd190615d1e0f4f052433408","tarball":"https://registry.npmjs.org/@aitianyu.cn/idp-node-sdk/-/idp-node-sdk-4.0.0.tgz","fileCount":6,"integrity":"sha512-jq8EVUKnUsb0KY7u+hlj+8tgqcq4ezIK+rrbtfzYDTO4BGTY/rxjp1+LcDQ0EnMV0VSDMc1xfoTv0FuVtDh8Rw==","signatures":[{"sig":"MEUCIQCjeskNBHOF8Uia5C8z6PD0YgBp5VFvRbVn0BKsAbYeYwIgFRkFLtfFk2QMM9NqEE65pDKowKsX1Q90kP5Ragrh2BY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":129304},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"6a989efd5c96839cc2fc0147436d5e2633748084","private":false,"scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","clean":"rm -rf dist","type-check":"tsc --noEmit"},"_npmUser":{"name":"aitianyu.cn","email":"product@aitianyu.cn"},"_npmVersion":"11.5.1","description":"天宇 IdP Node.js SDK","directories":{},"_nodeVersion":"24.5.0","dependencies":{"jose":"^5.9.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/idp-node-sdk_4.0.0_1785639444646_0.3472106436797535","host":"s3://npm-registry-packages-npm-production"}},"4.1.0":{"name":"@aitianyu.cn/idp-node-sdk","version":"4.1.0","keywords":["oauth2","oidc","authentication","tianyu","idp"],"license":"MIT","_id":"@aitianyu.cn/idp-node-sdk@4.1.0","maintainers":[{"name":"aitianyu.cn","email":"product@aitianyu.cn"}],"dist":{"shasum":"d14771f692efb4332c098e15195ee93be3150883","tarball":"https://registry.npmjs.org/@aitianyu.cn/idp-node-sdk/-/idp-node-sdk-4.1.0.tgz","fileCount":6,"integrity":"sha512-gxdvDRYP8YfnGbyHrtWpqJ1lCnbYofXbDbWtuaIpzNZja8lbRvfAtrhG3Xwx1M9q5RTu3VyxFwUaFdaB7P6tFw==","signatures":[{"sig":"MEUCIQCThrEyk7rATxGStbnS7roKw8vbwKEIXkAsl5uJ8J5K8QIgGzDokWOvE3fpY7JDV2UamAaecoukKtMDlDW9wUx8Bjw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":128665},"main":"./dist/index.cjs","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4362c1fa2fab7a09a3222936fc353433463a4063","private":false,"scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","test":"vitest run","build":"tsup src/index.ts --format cjs,esm --dts --clean","clean":"rm -rf dist","type-check":"tsc --noEmit"},"_npmUser":{"name":"aitianyu.cn","email":"product@aitianyu.cn"},"_npmVersion":"11.5.1","description":"天宇 IdP Node.js SDK","directories":{},"_nodeVersion":"24.5.0","dependencies":{"jose":"^5.9.0"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^2.0.0","typescript":"^5.4.0","@types/node":"^20.0.0"},"_npmOperationalInternal":{"tmp":"tmp/idp-node-sdk_4.1.0_1785647965177_0.8012203324130125","host":"s3://npm-registry-packages-npm-production"}},"4.2.0":{"name":"@aitianyu.cn/idp-node-sdk","version":"4.2.0","description":"天宇 IdP Node.js SDK","license":"MIT","private":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":"./dist/index.js","require":"./dist/index.cjs","types":"./dist/index.d.ts"}},"scripts":{"build":"tsup src/index.ts --format cjs,esm --dts --clean","dev":"tsup src/index.ts --format cjs,esm --dts --watch","type-check":"tsc --noEmit","test":"vitest run","clean":"rm -rf dist"},"engines":{"node":">=18.0.0"},"keywords":["oauth2","oidc","authentication","tianyu","idp"],"dependencies":{"jose":"^5.9.0"},"devDependencies":{"@types/node":"^20.0.0","tsup":"^8.0.0","typescript":"^5.4.0","vitest":"^2.0.0"},"_id":"@aitianyu.cn/idp-node-sdk@4.2.0","gitHead":"bf742f771fa1ac79eedd225015276dc1717d802a","_nodeVersion":"24.5.0","_npmVersion":"11.5.1","dist":{"integrity":"sha512-sFvLJmGkIFRVlJOcpLaeEm0/ab4GOF3Gck5/m1yS/Yld/6wbF/R4kldisS6whRYPU+74AIWVE0DzC+7lM7i6uQ==","shasum":"a4247b9614d200ab8be083879108c9086ad8696d","tarball":"https://registry.npmjs.org/@aitianyu.cn/idp-node-sdk/-/idp-node-sdk-4.2.0.tgz","fileCount":6,"unpackedSize":128288,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCX6ms8QtmJQaO1uHaF89FuoUCIe8wtrNV7/WxGWiudBAIhAM89HD0/JazhO8oXAxTfdHkEz8nyg0XIEz6gQXfTSwwP"}]},"_npmUser":{"name":"aitianyu.cn","email":"product@aitianyu.cn"},"directories":{},"maintainers":[{"name":"aitianyu.cn","email":"product@aitianyu.cn"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/idp-node-sdk_4.2.0_1785765040345_0.16185593759974926"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-02T02:57:24.518Z","modified":"2026-08-03T13:50:40.643Z","0.1.0":"2026-07-09T06:54:47.516Z","4.0.0":"2026-08-02T02:57:24.788Z","4.1.0":"2026-08-02T05:19:25.302Z","4.2.0":"2026-08-03T13:50:40.504Z"},"license":"MIT","keywords":["oauth2","oidc","authentication","tianyu","idp"],"description":"天宇 IdP Node.js SDK","maintainers":[{"name":"aitianyu.cn","email":"product@aitianyu.cn"}],"readme":"# @aitianyu.cn/idp-node-sdk\n\n天宇 IdP Node.js SDK。用于服务端应用接入天宇 IdP，提供 Token 验证、用户信息获取、License 检查、Client Credentials、产品互调、消息签名与 Token Exchange 等能力。\n\n[![npm](https://img.shields.io/npm/v/@aitianyu.cn/idp-node-sdk)](https://registry.aitianyu.cn)\n[![Node.js](https://img.shields.io/badge/Node.js-18+-green)](https://nodejs.org)\n[![License](https://img.shields.io/badge/License-MIT-blue)](#license)\n\n---\n\n## 安装\n\n```bash\nnpm install @aitianyu.cn/idp-node-sdk\n```\n\n---\n\n## 快速开始\n\n```typescript\nimport { TianyuIdPClient } from \"@aitianyu.cn/idp-node-sdk\";\n\nconst idp = new TianyuIdPClient({\n    issuer: \"https://idp.tianyu.com\",\n    clientId: \"your-client-id\",\n    clientSecret: \"your-client-secret\",\n});\n\n// 验证 access_token 并获取用户上下文（含 License 信息）\nconst ctx = await idp.verifyAccessToken(\"eyJ...\");\n\n// 检查 scope\nidp.requireScope(\"report.read\")(ctx);\n\n// 检查 License 类型\nif (idp.hasLicenseType(ctx, \"cloud_enterprise\")) {\n    // 企业功能\n}\n```\n\n---\n\n## API\n\n### `new TianyuIdPClient(config)`\n\n| 参数                  | 类型     | 必填 | 说明                                          |\n| --------------------- | -------- | ---- | --------------------------------------------- |\n| `issuer`              | `string` | ✓    | IdP 服务地址                                  |\n| `clientId`            | `string` | ✓    | OAuth 客户端 ID                               |\n| `clientSecret`        | `string` |      | OAuth 客户端密钥（机密客户端 / 产品互调必填） |\n| `redirectUri`         | `string` |      | 登录回调地址（服务端授权码流程可选）          |\n| `algorithm`           | `string` |      | JWT 算法（默认 `RS256`）                      |\n| `userContextCacheTtl` | `number` |      | UserContext 缓存 TTL（秒），默认跟随 token 剩余有效期 |\n\n---\n\n## 用户 Token 验证\n\n### `verifyAccessToken(token, audience?)`\n\n本地验证 JWT 签名与有效期，首次自动回源 `/userinfo` 获取完整用户上下文（含 License），后续命中缓存返回。\n\n- 抛出 `TOKEN_EXPIRED` — Token 已过期\n- 抛出 `INVALID_TOKEN` — 签名或 issuer/audience 不合法\n\n```typescript\nconst ctx = await idp.verifyAccessToken(accessToken);\n// ctx.sub         — 用户 ID\n// ctx.tenant_id   — 租户 ID\n// ctx.scope       — 空格分隔的 scope 字符串\n// ctx.roles       — 角色列表\n// ctx.licenses    — License 列表（含类型、价格等级、allowed_scopes）\n```\n\n### `introspect(token)`\n\n调用 `/oauth2/introspect` 做远程令牌内省（适用于高安全性场景，会命中最新撤销状态）。\n\n---\n\n## Client Credentials（产品令牌）\n\n### `getClientCredentialsToken(scopes?)`\n\n使用 Client Credentials 模式获取产品服务令牌，自动缓存 + 并发合并（single-flight）。\n\n```typescript\nconst productToken = await idp.getClientCredentialsToken([\"orders.read\"]);\n```\n\n---\n\n## 产品互调（服务发现 + 调用）\n\n### `bootstrap(opts?)`\n\n产品启动时登记本实例：获取 Product Token → 上报 `endpoint_url` → 启动心跳（周期 = `ttlSeconds * 0.8`）。\n\n```typescript\nawait idp.bootstrap({\n    endpointUrl: \"https://product-a.internal:8080\",\n    region: \"cn-shanghai\",\n    ttlSeconds: 30,\n    capabilities: [\"orders.read\", \"orders.write\"],\n});\n```\n\n| 参数          | 说明                                             |\n| ------------- | ------------------------------------------------ |\n| `instanceId`  | 实例唯一标识，默认 `instance-<pid>`              |\n| `endpointUrl` | 本服务对外访问地址（供其他产品调用）             |\n| `region`      | 部署区域标记                                     |\n| `ttlSeconds`  | 心跳超时秒数，默认 30                            |\n| `capabilities`| 此实例声明支持的 scope                           |\n\n### `shutdown()`\n\n主动下线：停止心跳定时器并通知 IdP 摘除实例，建议挂载到 `SIGTERM`/`SIGINT` 钩子。\n\n### `resolveProductEndpoints(targetProductId)`\n\n查询目标产品的在线实例列表（本地缓存 30 秒）。\n\n### `callProduct(targetProductId, path, opts?)`\n\n调用另一个产品的服务：自动注入 Product Token、按权重挑选实例，可选消息签名与 Token Exchange。\n\n```typescript\nconst res = await idp.callProduct(\"product-b\", \"/orders\", {\n    method: \"POST\",\n    body: { sku: \"X\" },\n    sign: true,          // 附加消息签名\n    userToken: bearer,   // 自动做 Token Exchange，让下游感知用户上下文\n    timeoutMs: 3000,\n});\n```\n\n---\n\n## 产品 Token 接收方验证\n\n### `verifyProductToken(token)`\n\n本地 JWKS 验签 + 字段提取，返回 `ProductContext`（发起方 `client_id`、`product_name`）。\n\n- 抛出 `INVALID_TOKEN_TYPE` — 令牌非产品令牌\n\n### `productAuthMiddleware()`\n\nFastify/Express 兼容中间件：验证 Product Token 身份，成功后将 `productContext` 注入 `request`。\n**不做 scope 校验**——产品间调用的访问控制完全交由 IdP 信任机制（`isApiCallAllowed`）在 IdP 网关强制。\n\n```typescript\nfastify.addHook(\"preHandler\", idp.productAuthMiddleware());\n```\n\n---\n\n## 消息签名（L2 保护）\n\n### `getCertificateCredentials()`\n\n获取本产品证书 + 私钥（5 分钟缓存），供 mTLS 或消息签名使用。\n\n### `signPayload(payload)`\n\n用本产品私钥对消息体做 RSA-PSS（PS256）签名，返回 `{ signature, serial }`。\n\n### `verifySignature(payload, jws, certSerial)`\n\n验证签名：向 IdP 查询证书公钥（带缓存）→ 校验 JWS → 比较 payload 一致性。\n\n---\n\n## Token Exchange\n\n### `exchangeUserToken(userToken, targetProductId)`\n\n用用户 Token + 本产品身份获取发给目标产品的委托 Token；委托 Token 携带用户 `sub` 和 `actor`（本产品），目标产品可据此感知\"谁代表谁调用\"。结果自动缓存。\n\n---\n\n## License 检查 API\n\n### `getLicenses(ctx)`\n\n返回用户所有 License 列表（原始数据）。\n\n### `getLicenseTypes(ctx): string[]`\n\n返回用户拥有的 License 类型列表（去重）。\n\n### `hasLicenseType(ctx, type): boolean`\n\n检查用户是否拥有指定类型的**有效** License。\n\n```typescript\nif (idp.hasLicenseType(ctx, \"cloud_enterprise\")) {\n    /* 企业功能 */\n}\n```\n\n### `hasLicenseTier(ctx, type, priceTier): boolean`\n\n检查用户是否拥有指定类型 + 价格等级的有效 License。\n\n```typescript\nif (idp.hasLicenseTier(ctx, \"cloud_personal\", \"pro\")) {\n    /* pro 功能 */\n}\n```\n\n### `getLicenseByType(ctx, type)`\n\n返回用户在指定类型下的所有有效 License，用于判断最高订阅层级。\n\n### `hasScope(ctx, scope): boolean`\n\n检查用户是否拥有指定 scope。\n\n### `requireScope(scope)`\n\n返回一个守卫函数，scope 不满足时抛出 `403 INSUFFICIENT_SCOPE` 错误。\n\n```typescript\n// Fastify 示例\napp.get(\"/reports\", async (req) => {\n    const ctx = await idp.verifyAccessToken(req.headers.authorization!.slice(7));\n    idp.requireScope(\"report.read\")(ctx);\n    // ...\n});\n```\n\n---\n\n## 导出类型\n\n- `TianyuIdPClientOptions`\n- `UserContext` / `UserLicense`\n- `TokenResponse` / `IntrospectResponse` / `AccessTokenPayload`\n- `ProductContext`\n- `BootstrapOptions` / `CallProductOptions`\n- `ProductEndpoint` / `CertificateCredentials`\n- `SendEmailOptions` / `SendEmailResult` / `EmailAttachment`\n\n---\n\n## 邮件发送\n\n### `sendEmail(opts)`\n\n以当前产品身份，通过 IdP 邮件通道向指定用户发送邮件。IDP 根据 `userId` 解析收件人邮箱，并按租户类型自动选择发送通道：\n\n- **business tenant** 且配置了自定义 SMTP → 使用租户自定义通道\n- **其他** → 使用系统默认通道\n\n邮件内容由调用方完全控制，IDP 不修改内容。\n\n```typescript\n// 纯文本邮件\nconst result = await idp.sendEmail({\n    userId: \"user-uuid\",\n    title: \"您的权限申请已批准\",\n    payload: \"您申请的 reporting.read 权限已被批准，有效期 7 天。\",\n    html: \"<p>您申请的 <b>reporting.read</b> 权限已被批准，有效期 7 天。</p>\",\n});\n\n// 带附件\nawait idp.sendEmail({\n    userId: \"user-uuid\",\n    title: \"月度报表\",\n    payload: \"请查收附件中的月度报表。\",\n    attachments: [\n        {\n            filename: \"report-2026-06.pdf\",\n            content: pdfBuffer,          // Buffer 自动转为 Base64\n            contentType: \"application/pdf\",\n        },\n    ],\n});\n```\n\n| 参数          | 类型                  | 必填 | 说明                                      |\n| ------------- | --------------------- | ---- | ----------------------------------------- |\n| `userId`      | `string`              | ✓    | 目标用户 ID（IDP 内部 UUID）              |\n| `title`       | `string`              | ✓    | 邮件主题                                  |\n| `payload`     | `string`              |      | 纯文本正文                                |\n| `html`        | `string`              |      | HTML 正文                                 |\n| `attachments` | `EmailAttachment[]`   |      | 附件列表；`content` 为 Buffer 时自动 Base64 |\n\n**返回值 `SendEmailResult`：**\n\n```typescript\n{ success: true,  messageId: \"msg-001\" }\n{ success: false, error: \"USER_NOT_FOUND\" }\n```\n\n---\n\n## 在 Express 中使用\n\n```typescript\nimport express from \"express\";\nimport { TianyuIdPClient } from \"@aitianyu.cn/idp-node-sdk\";\n\nconst idp = new TianyuIdPClient({ issuer: \"https://idp.tianyu.com\", clientId: \"xxx\" });\n\nasync function authMiddleware(req, res, next) {\n    const token = req.headers.authorization?.replace(\"Bearer \", \"\");\n    if (!token) return res.status(401).json({ error: \"Unauthorized\" });\n    try {\n        req.user = await idp.verifyAccessToken(token);\n        next();\n    } catch {\n        res.status(401).json({ error: \"Invalid token\" });\n    }\n}\n\n// 检查 License 类型的守卫\nfunction requireLicense(type: string) {\n    return (req, res, next) => {\n        if (!idp.hasLicenseType(req.user, type)) {\n            return res.status(403).json({ error: \"License required\", required: type });\n        }\n        next();\n    };\n}\n\napp.get(\"/enterprise-feature\", authMiddleware, requireLicense(\"cloud_enterprise\"), (req, res) => {\n    res.json({ data: \"...\" });\n});\n```\n\n---\n\n## 环境要求\n\n- Node.js >= 18.0.0\n- ESM 或 CJS 均支持\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md"}