@h-ai/api-client 0.1.0-alpha.42 → 0.1.0-alpha.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { apiContract } from '@h-ai/api-contract';
|
|
2
2
|
import { core, err, ok } from '@h-ai/core';
|
|
3
3
|
import { createORPCClient, ORPCError } from '@orpc/client';
|
|
4
4
|
import { OpenAPILink } from '@orpc/openapi-client/fetch';
|
|
@@ -242,7 +242,7 @@ var HaiApiClientError = core.error.buildHaiErrorsDef("api-client", ApiClientErro
|
|
|
242
242
|
// src/api-client-main.ts
|
|
243
243
|
var DEFAULT_TIMEOUT = 3e4;
|
|
244
244
|
var DEFAULT_CLIENT_NAME = "hai-api-client";
|
|
245
|
-
var DEFAULT_REFRESH_PATH =
|
|
245
|
+
var DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh);
|
|
246
246
|
var TRAILING_SLASHES_REGEX = /\/+$/;
|
|
247
247
|
var tokenStorage = {
|
|
248
248
|
/** 内存存储——SSR / Node 测试或一次性会话使用。 */
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/api-client-auth.ts","../messages/en-US.json","../messages/zh-CN.json","../src/api-client-i18n.ts","../src/api-client-types.ts","../src/api-client-main.ts"],"names":["core"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @h-ai/api-client — Token 存储与刷新管理\n *\n * 提供两个内置 Token 存储适配器(localStorage / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n *\n * @example Node.js 测试\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\n * ```\n *\n * @module api-client-auth\n */\n\nimport type { TokenPair, TokenStorage } from './api-client-types.js'\nimport { core } from '@h-ai/core'\nimport { z } from 'zod'\n\nconst logger = core.logger.child({ module: 'api-client', scope: 'auth' })\n\n/**\n * httpOnly cookie 模式哨兵值。\n * `apiClient.tokenStorage.httpOnlyCookie()` 的 `getRefreshToken()` 返回此值,\n * 告知 `doRefresh` 应依赖浏览器自动发送的 httpOnly Cookie,\n * 而不是将 refreshToken 写入请求体。\n */\nconst HTTPONLY_COOKIE_SENTINEL = '__httponly_cookie__'\n\n// ─── localStorage 存储 ────────────────────────────────────────────────────────\n\nconst LS_ACCESS_KEY = 'hai_access_token'\nconst LS_REFRESH_KEY = 'hai_refresh_token'\n\n/**\n * 创建基于 `localStorage` 的 Token 存储(浏览器 SPA / PWA)。\n *\n * ⚠️ 安全提示(XSS 风险):\n * - `localStorage` 对同源脚本可读,任意 XSS 漏洞都可以偷取 Token。\n * - 生产环境应优先使用 httpOnly cookie + SameSite 或 BFF 主机代理。\n * - 本适配器仅适用于内部工具、Demo、或者已接受 XSS 风险的浏览器场景,\n * 并且在调用方明确选择使用。`apiClient.create` 默认不会使用本适配器,\n * 需调用方显式传入 `auth.storage = apiClient.tokenStorage.localStorage()`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n */\nexport function createLocalStorageTokenStorage(): TokenStorage {\n // 首次实例化时警告一次,提示调用方可能需要重新评估安全模型。\n logger.warn('apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.')\n return {\n async getAccessToken() {\n return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null\n },\n async setAccessToken(token) {\n globalThis.localStorage?.setItem(LS_ACCESS_KEY, token)\n },\n async getRefreshToken() {\n return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null\n },\n async setRefreshToken(token) {\n globalThis.localStorage?.setItem(LS_REFRESH_KEY, token)\n },\n async clear() {\n globalThis.localStorage?.removeItem(LS_ACCESS_KEY)\n globalThis.localStorage?.removeItem(LS_REFRESH_KEY)\n },\n }\n}\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建内存 Token 存储(Node.js 测试 / SSR / 短生命周期场景)。\n * 页面刷新后丢失。\n *\n * @example\n * ```ts\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\n * ```\n */\nexport function createMemoryTokenStorage(): TokenStorage {\n let accessToken: string | null = null\n let refreshToken: string | null = null\n return {\n async getAccessToken() { return accessToken },\n async setAccessToken(token) { accessToken = token },\n async getRefreshToken() { return refreshToken },\n async setRefreshToken(token) { refreshToken = token },\n async clear() {\n accessToken = null\n refreshToken = null\n },\n }\n}\n\n// ─── httpOnly Cookie 存储 ────────────────────────────────────────────────────\n\n/**\n * 创建基于 httpOnly cookie 的 Token 存储(生产环境推荐方案)。\n *\n * **安全模型:**\n * - `accessToken` 保存在**内存**中:JS 可读写,用于注入 `Authorization: Bearer`;\n * 页面刷新后丢失,由 refresh 流程自动补充。\n * - `refreshToken` 保存在服务端设置的 **httpOnly cookie** 中:\n * - JS 不可读写,XSS 脚本无法直接盗取。\n * - 结合 `SameSite=Strict` 可阻止 CSRF 攻击。\n * - 浏览器在每次请求时自动携带,无需客户端显式传递。\n *\n * **服务端配套要求:**\n * 1. 登录端点在响应中设置:\n * ```\n * Set-Cookie: refresh_token=<token>; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=<seconds>\n * ```\n * `Path` 限定为 refresh 端点,减少 Cookie 暴露面。\n * 2. Refresh 端点从 Cookie 读取 refresh token(忽略请求体中的 `refreshToken` 字段)。\n * 3. Logout 端点清除 Cookie:\n * ```\n * Set-Cookie: refresh_token=; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=0\n * ```\n *\n * **跨域注意事项:**\n * 若 API 与前端不同源,服务端还需设置:\n * `Access-Control-Allow-Origin: <前端域名>` 和 `Access-Control-Allow-Credentials: true`。\n * 调用方不需要做额外配置,`apiClient.create` 会自动识别并设置 `credentials: 'include'`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\n * ```\n */\nexport function createHttpOnlyCookieTokenStorage(): TokenStorage {\n // access token 保存在内存中,用于注入 Authorization 请求头\n let memoryAccessToken: string | null = null\n return {\n async getAccessToken() { return memoryAccessToken },\n async setAccessToken(token) { memoryAccessToken = token },\n // refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;\n // JS 不可见,返回哨兵值让 TokenManager 识别此模式。\n async getRefreshToken() { return HTTPONLY_COOKIE_SENTINEL },\n // refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。\n async setRefreshToken(_token) { /* 由服务端 Set-Cookie 响应头管理,客户端无需操作 */ },\n async clear() {\n memoryAccessToken = null\n // httpOnly cookie 的清除必须由服务端 logout 端点完成(Set-Cookie: Max-Age=0);\n // 客户端无法通过 JS 直接删除 httpOnly cookie。\n },\n }\n}\n\n// ─── Token 管理器 ─────────────────────────────────────────────────────────────\n\n/** 校验 refresh 响应体中的 Token 字段;httpOnly cookie 模式允许不返回 refreshToken。 */\nconst RefreshTokenPayloadSchema = z.object({\n accessToken: z.string().min(1),\n refreshToken: z.string().min(1).optional(),\n expiresIn: z.number().optional(),\n tokenType: z.string().optional(),\n})\n\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建 Token 管理器:封装存储读写、自动刷新、并发去重与回调通知。\n *\n * 主要供 `apiClient.create` 内部使用,同时作为 named export 暴露给测试与高级用户。\n *\n * 行为约定:\n * - 并发多次 `refresh()` 共享同一个请求。\n * - refresh 接口失败、响应字段缺失或 401,都视为刷新失败:清空存储并触发 `onRefreshFailed`。\n * - 兼容两种响应格式:`{ data: tokens }` 与 `{ data: { tokens } }`(HaiResult 包装)。\n *\n * @example\n * ```ts\n * const manager = createTokenManager(\n * apiClient.tokenStorage.memory(),\n * 'https://api.example.com/api/v1/auth/refresh',\n * fetch,\n * () => notifyLoginRequired(),\n * )\n *\n * const off = manager.onTokenRefreshed(tokens => syncTokens(tokens))\n * const tokens = await manager.refresh()\n * off()\n * ```\n *\n * @param storage - Token 存储适配器\n * @param refreshEndpointUrl - refresh 接口完整 URL(`baseUrl + refreshPath`)\n * @param fetchFn - fetch 实现(允许注入 mock)\n * @param onRefreshFailed - 刷新失败回调\n * @param refreshTimeoutMs - 刷新请求超时(ms),默认 10_000\n * @returns Token 管理器实例\n */\nexport function createTokenManager(\n storage: TokenStorage,\n refreshEndpointUrl: string,\n fetchFn: typeof globalThis.fetch,\n onRefreshFailed?: () => void,\n refreshTimeoutMs: number = 10_000,\n) {\n const callbacks: RefreshCallback[] = []\n let refreshPromise: Promise<TokenPair | null> | null = null\n\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n refreshPromise = doRefresh()\n try {\n return await refreshPromise\n }\n finally {\n refreshPromise = null\n }\n }\n\n async function doRefresh(): Promise<TokenPair | null> {\n const refreshToken = await storage.getRefreshToken()\n if (!refreshToken) {\n logger.warn('Token refresh skipped, no refresh token available')\n onRefreshFailed?.()\n return null\n }\n\n logger.debug('Refreshing token')\n // 超时保护:避免 refresh 接口挂死时 dedup Promise 永不 resolve,连带所有等待者 hang。\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs)\n try {\n // httpOnly cookie 模式:浏览器自动发送 Cookie,请求体无需携带 refreshToken;\n // 同时设置 credentials:'include' 以支持跨域 Cookie 发送。\n const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL\n const requestInit: RequestInit = {\n method: 'POST',\n body: useHttpOnlyCookie ? undefined : JSON.stringify({ refreshToken }),\n credentials: useHttpOnlyCookie ? 'include' : 'same-origin',\n signal: controller.signal,\n }\n if (!useHttpOnlyCookie) {\n requestInit.headers = { 'Content-Type': 'application/json' }\n }\n const response = await fetchFn(refreshEndpointUrl, requestInit)\n\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await storage.clear()\n onRefreshFailed?.()\n }\n else {\n logger.warn('Token refresh transient failure, keeping tokens', { status: response.status })\n }\n return null\n }\n\n const body = await response.json() as { data?: unknown }\n const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data))\n if (!parsed.success) {\n logger.warn('Token refresh returned invalid data', { issues: parsed.error.issues })\n await storage.clear()\n onRefreshFailed?.()\n return null\n }\n if (!useHttpOnlyCookie && !parsed.data.refreshToken) {\n logger.warn('Token refresh returned invalid data', { issues: [{ path: ['refreshToken'], message: 'Required' }] })\n await storage.clear()\n onRefreshFailed?.()\n return null\n }\n\n // 默认值兜底,避免不安全强转\n const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL\n const tokens: TokenPair = {\n accessToken: parsed.data.accessToken,\n refreshToken: refreshTokenForStorage,\n expiresIn: parsed.data.expiresIn ?? 3600,\n tokenType: 'Bearer',\n }\n await storage.setAccessToken(tokens.accessToken)\n await storage.setRefreshToken(refreshTokenForStorage)\n logger.info('Token refreshed successfully')\n for (const cb of callbacks) cb(tokens)\n return tokens\n }\n catch (error) {\n // 网络或 fetch 本身抛异常(超时、断网、CORS)视为可重试的暂时性错误,\n // 不清除 Token,仅记录日志。调用方可在下次请求时重试。\n logger.error('Token refresh failed (transient)', { error })\n return null\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n storage,\n refresh,\n async setTokens(tokens: TokenPair) {\n await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken)\n await storage.setRefreshToken(tokens.refreshToken)\n },\n async clear() {\n await storage.clear()\n },\n onTokenRefreshed(callback: RefreshCallback): () => void {\n callbacks.push(callback)\n return () => {\n const idx = callbacks.indexOf(callback)\n if (idx >= 0)\n callbacks.splice(idx, 1)\n }\n },\n }\n}\n\n/** Token 管理器类型(`createTokenManager` 返回值)。 */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/** 兼容 `{ data: tokens }` 与 `{ data: { tokens } }` 两种 refresh 响应体。 */\nfunction readTokenPayload(data: unknown): unknown {\n if (typeof data === 'object' && data !== null && 'tokens' in data) {\n return (data as { tokens: unknown }).tokens\n }\n return data\n}\n","{\n \"apiClient_networkError\": \"Network request failed\",\n \"apiClient_timeout\": \"Request timeout\",\n \"apiClient_serverError\": \"Server error ({status})\",\n \"apiClient_unauthorized\": \"Unauthorized, please log in again\",\n \"apiClient_forbidden\": \"Access forbidden\",\n \"apiClient_notFound\": \"Resource not found\",\n \"apiClient_validationFailed\": \"Request validation failed\",\n \"apiClient_tokenRefreshFailed\": \"Token refresh failed, please log in again\",\n \"apiClient_notInitialized\": \"API client not initialized, please call apiClient.init() first\",\n \"apiClient_alreadyInitialized\": \"API client is already initialized; call apiClient.close() before init()\",\n \"apiClient_configError\": \"API client config error: {error}\",\n \"apiClient_responseValidationFailed\": \"Response validation failed\",\n \"apiClient_unknown\": \"Unknown error\"\n}\n","{\n \"apiClient_networkError\": \"网络请求失败\",\n \"apiClient_timeout\": \"请求超时\",\n \"apiClient_serverError\": \"服务器错误({status})\",\n \"apiClient_unauthorized\": \"未授权,请重新登录\",\n \"apiClient_forbidden\": \"无权限访问\",\n \"apiClient_notFound\": \"请求的资源不存在\",\n \"apiClient_validationFailed\": \"请求参数校验失败\",\n \"apiClient_tokenRefreshFailed\": \"Token刷新失败,请重新登录\",\n \"apiClient_notInitialized\": \"API 客户端未初始化,请先调用 apiClient.init()\",\n \"apiClient_alreadyInitialized\": \"API 客户端已初始化,请先调用 apiClient.close() 再重新初始化\",\n \"apiClient_configError\": \"API 客户端配置错误:{error}\",\n \"apiClient_responseValidationFailed\": \"响应数据校验失败\",\n \"apiClient_unknown\": \"未知错误\"\n}\n","/**\n * @h-ai/api-client — i18n\n *\n * 模块 i18n 文案访问入口。\n * @module api-client-i18n\n */\n\nimport { core } from '@h-ai/core'\nimport messagesEnUS from '../messages/en-US.json'\nimport messagesZhCN from '../messages/zh-CN.json'\n\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * api-client 模块 i18n 文案获取器\n *\n * @param key - 文案 Key\n * @param options - 插值参数\n * @returns 本地化后的文案字符串\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\n})\n","/**\n * @h-ai/api-client — 类型与错误码\n *\n * 公共类型(TokenPair、TokenStorage、ApiClientConfig 等)与标准错误集合(HaiApiClientError)。\n * 不包含实现,仅用于跨文件共享与对外导出。\n * @module api-client-types\n */\n\nimport type { ErrorInfo, HaiResult } from '@h-ai/core'\nimport type { CryptoFunctions } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport { core } from '@h-ai/core'\n\n// ─── 错误码 ───────────────────────────────────────────────────────────────────\n\nconst ApiClientErrorInfo = {\n NETWORK_ERROR: '001:502',\n TIMEOUT: '002:504',\n SERVER_ERROR: '003:502',\n UNAUTHORIZED: '004:401',\n FORBIDDEN: '005:403',\n NOT_FOUND: '006:404',\n VALIDATION_FAILED: '007:400',\n TOKEN_REFRESH_FAILED: '008:401',\n NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * API client 标准错误集合(遵循 `@h-ai/core` 错误码规范)。\n *\n * @example\n * ```ts\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * redirectToLogin()\n * }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/** Token 对(与 `@h-ai/iam` 的 TokenPair 对齐)。 */\nexport interface TokenPair {\n /** 短期访问凭证,请求时作为 `Authorization: Bearer ...`。 */\n readonly accessToken: string\n /** 长期刷新凭证;httpOnly cookie 模式下由服务端管理,响应体中可能不存在。 */\n readonly refreshToken?: string\n /** accessToken 有效期秒数。 */\n readonly expiresIn: number\n /** 固定为 `'Bearer'`。 */\n readonly tokenType: 'Bearer'\n}\n\n/**\n * Token 存储适配器。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n * - `apiClient.tokenStorage.localStorage()` — 浏览器 SPA / PWA(有 XSS 风险,生产不推荐)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n */\nexport interface TokenStorage {\n readonly getAccessToken: () => Promise<string | null>\n readonly setAccessToken: (token: string) => Promise<void>\n readonly getRefreshToken: () => Promise<string | null>\n readonly setRefreshToken: (token: string) => Promise<void>\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/** Token 自动刷新配置。 */\nexport interface AuthConfig {\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器 SPA 如确需持久化,可显式传 `apiClient.tokenStorage.localStorage()`,\n * 但请先评估 XSS 风险。\n * - Capacitor / 小程序请使用对应平台的安全存储适配器(如 `@h-ai/capacitor`)。\n */\n readonly storage?: TokenStorage\n /** 刷新接口路径,相对于 `baseUrl`,默认 `/auth/refresh`。 */\n readonly refreshPath?: string\n /**\n * 刷新请求超时(ms),默认 10_000。\n * 避免 refresh 接口上游挂死时所有 dedup 调用方一起 hang。\n * 超时后按现有 transient 失败处理(不清 token,下次请求可重试)。\n */\n readonly refreshTimeoutMs?: number\n /** 刷新成功回调,用于同步状态(例如多 Tab 广播)。 */\n readonly onTokenRefreshed?: (tokens: TokenPair) => void\n /** 刷新失败回调(如 refresh token 已失效),通常用于跳转登录页。 */\n readonly onRefreshFailed?: () => void\n}\n\n/** API client 初始化配置。 */\nexport interface ApiClientConfig {\n /** API service 完整 baseUrl,通常含 `apiPrefix`,例如 `https://api.example.com/api/v1`。 */\n readonly baseUrl: string\n /** 认证配置;不传则不启用 Token 注入与自动刷新。 */\n readonly auth?: AuthConfig\n /** 请求超时(ms),默认 30000。 */\n readonly timeout?: number\n /** 客户端标识,写入 `x-request-client` 请求头,默认 `hai-api-client`。 */\n readonly clientName?: string\n /** 额外请求头,可为静态对象或异步函数。 */\n readonly headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)\n /** 自定义 fetch(测试 mock / Capacitor / 小程序桥接)。 */\n readonly fetch?: typeof globalThis.fetch\n /**\n * 启用端到端传输加密(opt-in)。\n *\n * 配置后所有出站请求会在底层 fetch 之上做一次混合加密,并自动解密\n * 标记为 `X-Encrypted: true` 的响应。服务端需启用对应的 `serv.createApp({ transport })`。\n *\n * `crypto` 字段结构与 `@h-ai/crypto` 的 `crypto` 实例兼容;通常直接传入:\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n *\n * await crypto.init()\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * transport: { crypto },\n * })\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/** {@link ApiClientConfig.transport} 的配置项。 */\nexport interface ApiClientTransportConfig {\n /**\n * crypto 服务实例(通常直接传入 `@h-ai/crypto` 的 `crypto`)。\n * api-client 通过 `crypto.transport.createClient(...)` 创建会话。\n */\n readonly crypto: CryptoFunctions\n /**\n * 密钥协商端点路径,相对于 `baseUrl`,默认 `/_hai/key-exchange`。\n *\n * 必须与服务端 `serv.createApp({ transport: { keyExchangePath } })` 一致。\n */\n readonly keyExchangePath?: string\n}\n\n// ─── Client 接口 ──────────────────────────────────────────────────────────────\n\n/** Token 辅助操作。 */\nexport interface ApiClientAuth {\n /** 手动写入 Token(例如登录成功后)。 */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /** 清空存储的 Token(登出)。 */\n readonly clear: () => Promise<void>\n /** 订阅 Token 刷新成功事件,返回取消订阅函数。 */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/** API client 生命周期能力。 */\nexport interface ApiClientLifecycle {\n /**\n * 初始化 client;必须在调用任何 procedure 前完成。\n *\n * @example\n * ```ts\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * timeout: 15_000,\n * })\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /** 释放资源并允许重新 `init`。 */\n readonly close: () => Promise<void>\n /** 当前脱敏后的运行配置快照;未初始化时为 `null`。 */\n readonly config: ApiClientConfig | null\n /** 是否已完成初始化。 */\n readonly isInitialized: boolean\n /** Token 辅助操作。 */\n readonly auth: ApiClientAuth\n}\n\n/** 根据 oRPC contract 推导出的 typed API client(生命周期 + procedure 调用)。 */\nexport type ApiClient<TContract extends AnyContractRouter>\n = ApiClientLifecycle & JsonifiedClient<ContractRouterClient<TContract>>\n","/**\n * @h-ai/api-client — 主实现\n *\n * 包含两部分:\n * - `createApiClient(contract)`:根据 oRPC contract 创建 typed client 工厂(内部实现)。\n * - `apiClient`:统一公开入口;自身就是默认单例,同时挂载 `create` / `tokenStorage`。\n *\n * Client 同时具备生命周期能力(init/close/config/isInitialized/auth)和 contract\n * 推导出的 procedure 嵌套调用能力(如 `client.iam.auth.login(input)`)。\n *\n * 内置能力:\n * - 401 自动 refresh + 单次重试\n * - 并发 refresh 共享 Promise\n * - 请求超时(AbortController)\n * - `ORPCError` → `HaiApiClientError` 标准化映射\n * - 未初始化时调用 procedure 返回 `NOT_INITIALIZED`,不抛异常\n *\n * @example 默认单例\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const me = await apiClient.iam.auth.currentUser()\n * await apiClient.close()\n * ```\n *\n * @example 自定义 contract\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * ```\n *\n * @module api-client-main\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { TransportClient } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport type { TokenManager } from './api-client-auth.js'\nimport type {\n ApiClient,\n ApiClientAuth,\n ApiClientConfig,\n ApiClientLifecycle,\n} from './api-client-types.js'\nimport { apiContract, IAM_AUTH_ROUTES } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createLocalStorageTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.js'\nimport { HaiApiClientError } from './api-client-types.js'\n\nconst DEFAULT_TIMEOUT = 30_000\nconst DEFAULT_CLIENT_NAME = 'hai-api-client'\nconst DEFAULT_REFRESH_PATH = IAM_AUTH_ROUTES.refresh\nconst TRAILING_SLASHES_REGEX = /\\/+$/\n\n/** Token 存储方案集合:内存 / 浏览器 localStorage / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */\n localStorage: createLocalStorageTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 根据 oRPC contract 创建 typed API client。\n *\n * 返回对象同时具备:\n * - **生命周期能力**:`init` / `close` / `config` / `isInitialized` / `auth`\n * - **contract procedure**:按 contract 嵌套结构调用,例如 `client.iam.auth.login(input)`\n *\n * @example 基础用法\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n *\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * if (result.success) {\n * await client.auth.setTokens(result.data.tokens)\n * }\n *\n * await client.close()\n * ```\n *\n * @example 自定义 fetch / 超时\n * ```ts\n * const client = apiClient.create(contract)\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * fetch: customFetch,\n * timeout: 10_000,\n * })\n * ```\n */\nexport function createApiClient<const TContract extends AnyContractRouter>(\n contract: TContract,\n): ApiClient<TContract> {\n // 客户端运行时状态\n const state: {\n config: ApiClientConfig | null\n rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null\n tokenManager: TokenManager | undefined\n transport: TransportClient | undefined\n } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\n }\n\n const auth: ApiClientAuth = {\n async setTokens(tokens) { await state.tokenManager?.setTokens(tokens) },\n async clear() { await state.tokenManager?.clear() },\n onTokenRefreshed(callback) {\n return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {})\n },\n }\n\n let initPromise: Promise<HaiResult<void>> | null = null\n const lifecycle: ApiClientLifecycle = {\n async init(config) {\n // 并发重入防护:同时多次 init() 只执行一次。\n // 另一个调用方 await 同一个 in-flight Promise,避免重复创建 raw client / token manager。\n if (initPromise)\n return initPromise\n // 已初始化后必须先 close() 再重新 init,避免旧 transport 会话被新状态覆盖后无法 destroy。\n if (state.rawClient) {\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_alreadyInitialized'),\n )\n }\n initPromise = (async (): Promise<HaiResult<void>> => {\n let nextTransport: TransportClient | undefined\n try {\n nextTransport = buildTransportSession(config)\n const nextTokenManager = buildTokenManager(config, nextTransport)\n const nextRawClient = buildRawClient(contract, config, nextTokenManager, nextTransport)\n\n // 所有可能失败的构建步骤完成后再提交状态,避免调用方看到半初始化 client。\n state.config = config\n state.transport = nextTransport\n state.tokenManager = nextTokenManager\n state.rawClient = nextRawClient\n return ok(undefined)\n }\n catch (error) {\n // 构建过程中如果 transport 已创建但后续失败,必须主动释放,避免会话/定时器泄漏。\n nextTransport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_configError', { params: { error: String(error) } }),\n error,\n )\n }\n })()\n try {\n return await initPromise\n }\n finally {\n initPromise = null\n }\n },\n async close() {\n state.transport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n },\n get config() { return state.config ? core.sanitize.sanitizeSensitiveFields(state.config) : null },\n get isInitialized() { return state.rawClient !== null },\n auth,\n }\n\n /**\n * 顶层 Proxy:把“一个对象同时扮演两种角色”的需求揉到一起。\n *\n * 这个 client 既要像一个普通服务对象,暴露生命周期方法:\n * - `client.init()`\n * - `client.close()`\n * - `client.config`\n * - `client.isInitialized`\n * - `client.auth`\n *\n * 又要像一个按 contract 动态展开的 oRPC client,支持:\n * - `client.iam.auth.login(input)`\n * - `client.storage.object.put(input)`\n * - `client.ai.chat.send(input)`\n *\n * 由于第二类 API 的层级结构是“运行时按属性链逐步访问”的,无法靠写死对象字面量覆盖所有路径,\n * 所以这里使用 Proxy:\n * - 先拦截顶层属性访问;\n * - 如果访问的是生命周期字段,就直接返回 lifecycle 中的真实成员;\n * - 否则把这个属性当成 contract 路径的第一段,继续交给 `createProcedureProxy()` 处理。\n *\n * 换句话说:\n * - `client.init` → 返回 lifecycle.init\n * - `client.auth` → 返回 lifecycle.auth\n * - `client.iam` → 返回一个新的嵌套 Proxy,继续等待 `.auth.login(...)`\n *\n * 注意:如果将来 contract 顶层字段和 lifecycle 字段重名,这里会“优先返回 lifecycle”。\n */\n return new Proxy({}, {\n get(_target, property) {\n // Proxy 的属性键不一定是 string,也可能是 Symbol(例如某些运行时内部探测)。\n // 本 client 只处理字符串路径;非字符串键直接忽略,避免把 Symbol 误当成 API 路径的一部分。\n if (typeof property !== 'string')\n return undefined\n\n // 顶层字段命中 lifecycle 时,直接返回真实成员。\n // 例如:\n // - property === 'init' → 返回 init 函数\n // - property === 'auth' → 返回 auth 对象\n // 这样 `client.init()` / `client.auth.clear()` 就能像普通对象一样工作。\n if (property in lifecycle) {\n return Reflect.get(lifecycle, property)\n }\n\n // 走到这里,说明它不是生命周期字段,那就把它当成 contract 路径的第一段。\n // 例如访问 `client.iam` 时,这里会创建一个 path = ['iam'] 的嵌套 Proxy。\n // 后续如果继续访问 `.auth.login`,path 会逐步变成:\n // ['iam', 'auth'] → ['iam', 'auth', 'login']\n // 最终在函数调用时再映射到真正的 rawClient.iam.auth.login(...args)。\n return createProcedureProxy(state, [property])\n },\n // 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:\n // - 一部分成员来自 lifecycle\n // - 另一部分成员来自 contract 推导出的 procedure 层级\n // 因此使用类型断言把这个动态对象声明为最终对外的 typed client。\n }) as ApiClient<TContract>\n}\n\n// ─── 默认单例 ─────────────────────────────────────────────────────────────────\n\n/**\n * 默认 API typed client:绑定 `iam` / `storage` / `ai` 三个领域。\n *\n * 大多数应用只需 `import { apiClient }` 并调用 `apiClient.init()` 即可。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * const result = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * await apiClient.close()\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/** `apiClient` 默认单例 + 工厂命名空间的统一对外类型。 */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n}\n\n/** hai-framework API client 统一入口(默认单例 + `create` + `tokenStorage`)。 */\nexport const apiClient = new Proxy(createApiClient(defaultApiContract) as ApiClient<typeof defaultApiContract>, {\n get(target, property, receiver) {\n if (property === 'create')\n return createApiClient\n if (property === 'tokenStorage')\n return tokenStorage\n return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造嵌套 procedure 代理。\n *\n * oRPC client 接口是任意层嵌套的属性 + 末端调用(如 `client.iam.auth.login(input)`)。\n * 这里用统一的 Proxy 透传所有路径,叶子节点(被 apply 时)映射到\n * `state.rawClient[a][b][c](input)` 并把错误标准化为 `HaiResult`。\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null },\n path: string[],\n): unknown {\n return new Proxy(() => undefined, {\n get(_t, property) {\n if (typeof property !== 'string')\n return undefined\n return createProcedureProxy(state, [...path, property])\n },\n apply(_t, _thisArg, args) {\n return callProcedure(state.rawClient, path, args)\n },\n })\n}\n\nasync function callProcedure(\n rawClient: unknown,\n path: string[],\n args: unknown[],\n): Promise<unknown> {\n if (!rawClient) {\n return err(HaiApiClientError.NOT_INITIALIZED, apiClientM('apiClient_notInitialized'))\n }\n\n // 沿 path 解析 rawClient[a][b][c]...\n let target: unknown = rawClient\n for (const segment of path) {\n if ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n target = Reflect.get(target as object, segment)\n }\n if (typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n\n try {\n return await (target as (...a: unknown[]) => Promise<unknown>)(...args)\n }\n catch (error) {\n return mapClientError(error)\n }\n}\n\n// ─── 内部:构建 raw client 与 token manager ──────────────────────────────────\n\n/** 根据 `auth` 配置决定是否创建 TokenManager。 */\nfunction buildTokenManager(config: ApiClientConfig, transport: TransportClient | undefined): TokenManager | undefined {\n if (!config.auth)\n return undefined\n\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n // 默认使用 httpOnly cookie 存储(推荐方案:refresh token 由服务端管理,防 XSS)。\n // SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n // 浏览器 localStorage 场景请显式传入 `apiClient.tokenStorage.localStorage()`(有 XSS 风险)。\n const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage()\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH\n const refreshUrl = `${baseUrl}${refreshPath.startsWith('/') ? refreshPath : `/${refreshPath}`}`\n\n const manager = createTokenManager(storage, refreshUrl, fetchFn, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs)\n if (config.auth.onTokenRefreshed) {\n manager.onTokenRefreshed(config.auth.onTokenRefreshed)\n }\n return manager\n}\n\n/** 根据 `transport` 配置创建传输加密会话。 */\nfunction buildTransportSession(config: ApiClientConfig): TransportClient | undefined {\n if (!config.transport)\n return undefined\n const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis)\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const path = config.transport.keyExchangePath ?? '/_hai/key-exchange'\n const keyExchangeUrl = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`\n return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch })\n}\n\n/** 构造 oRPC OpenAPILink + raw client。 */\nfunction buildRawClient<TContract extends AnyContractRouter>(\n contract: TContract,\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): JsonifiedClient<ContractRouterClient<TContract>> {\n const link = new OpenAPILink(contract, {\n url: config.baseUrl,\n headers: async () => buildHeaders(config, tokenManager),\n fetch: async (request, init) => fetchWithRefresh(request, init, config, tokenManager, transport),\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 组装请求头:用户配置 + 客户端标识 + Bearer Token。 */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...configured,\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/** 401 自动 refresh 并重试一次。 */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager ? request.clone() : undefined\n const response = await fetchWithTimeout(request, init, config, transport)\n if (response.status !== 401 || !tokenManager || !retryRequest)\n return response\n\n const tokens = await tokenManager.refresh()\n if (!tokens)\n return response\n\n const headers = new Headers(retryRequest.headers)\n headers.set('authorization', `Bearer ${tokens.accessToken}`)\n return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n}\n\n/** fetch + AbortController 超时控制(联动调用方原始 signal)。 */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // 启用传输加密时,用 encryptedFetch 作为实际 fetch;\n // 它会首次请求前自动完成密钥协商,后续请求复用会话。\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n const timeout = config.timeout ?? DEFAULT_TIMEOUT\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n // 保留调用方原始 signal 的中断能力:上游主动 abort 也能控制超时 controller。\n const userSignal = request.signal\n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n }\n else {\n userSignal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n }\n try {\n return await fetchFn(new Request(request, { signal: controller.signal }), init)\n }\n finally {\n clearTimeout(timeoutId)\n }\n}\n\n/** ORPCError / AbortError / 其他异常 → 标准化 HaiResult 错误。 */\nfunction mapClientError(error: unknown) {\n if (error instanceof ORPCError) {\n // 保留 ORPCError 的原始 code / status / data,供上层区分具体业务异常(不仅是 HTTP 状态码)。\n const detail = { orpcCode: error.code, status: error.status, data: error.data }\n if (error.status === 401)\n return err(HaiApiClientError.UNAUTHORIZED, error.message, detail)\n if (error.status === 403)\n return err(HaiApiClientError.FORBIDDEN, error.message, detail)\n if (error.status === 404)\n return err(HaiApiClientError.NOT_FOUND, error.message, detail)\n return err(HaiApiClientError.SERVER_ERROR, error.message, detail)\n }\n if (error instanceof DOMException && error.name === 'AbortError') {\n return err(HaiApiClientError.TIMEOUT, apiClientM('apiClient_timeout'), error)\n }\n return err(HaiApiClientError.NETWORK_ERROR, apiClientM('apiClient_networkError'), error)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/api-client-auth.ts","../messages/en-US.json","../messages/zh-CN.json","../src/api-client-i18n.ts","../src/api-client-types.ts","../src/api-client-main.ts"],"names":["core"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @h-ai/api-client — Token 存储与刷新管理\n *\n * 提供两个内置 Token 存储适配器(localStorage / 内存)和一个 Token 管理器\n * (封装存储读写、自动刷新、并发去重、回调通知)。\n *\n * @example 浏览器 SPA\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n *\n * @example Node.js 测试\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\n * ```\n *\n * @module api-client-auth\n */\n\nimport type { TokenPair, TokenStorage } from './api-client-types.js'\nimport { core } from '@h-ai/core'\nimport { z } from 'zod'\n\nconst logger = core.logger.child({ module: 'api-client', scope: 'auth' })\n\n/**\n * httpOnly cookie 模式哨兵值。\n * `apiClient.tokenStorage.httpOnlyCookie()` 的 `getRefreshToken()` 返回此值,\n * 告知 `doRefresh` 应依赖浏览器自动发送的 httpOnly Cookie,\n * 而不是将 refreshToken 写入请求体。\n */\nconst HTTPONLY_COOKIE_SENTINEL = '__httponly_cookie__'\n\n// ─── localStorage 存储 ────────────────────────────────────────────────────────\n\nconst LS_ACCESS_KEY = 'hai_access_token'\nconst LS_REFRESH_KEY = 'hai_refresh_token'\n\n/**\n * 创建基于 `localStorage` 的 Token 存储(浏览器 SPA / PWA)。\n *\n * ⚠️ 安全提示(XSS 风险):\n * - `localStorage` 对同源脚本可读,任意 XSS 漏洞都可以偷取 Token。\n * - 生产环境应优先使用 httpOnly cookie + SameSite 或 BFF 主机代理。\n * - 本适配器仅适用于内部工具、Demo、或者已接受 XSS 风险的浏览器场景,\n * 并且在调用方明确选择使用。`apiClient.create` 默认不会使用本适配器,\n * 需调用方显式传入 `auth.storage = apiClient.tokenStorage.localStorage()`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.localStorage() },\n * })\n * ```\n */\nexport function createLocalStorageTokenStorage(): TokenStorage {\n // 首次实例化时警告一次,提示调用方可能需要重新评估安全模型。\n logger.warn('apiClient.tokenStorage.localStorage: storing tokens in localStorage exposes them to XSS. Prefer httpOnly cookies for production.')\n return {\n async getAccessToken() {\n return globalThis.localStorage?.getItem(LS_ACCESS_KEY) ?? null\n },\n async setAccessToken(token) {\n globalThis.localStorage?.setItem(LS_ACCESS_KEY, token)\n },\n async getRefreshToken() {\n return globalThis.localStorage?.getItem(LS_REFRESH_KEY) ?? null\n },\n async setRefreshToken(token) {\n globalThis.localStorage?.setItem(LS_REFRESH_KEY, token)\n },\n async clear() {\n globalThis.localStorage?.removeItem(LS_ACCESS_KEY)\n globalThis.localStorage?.removeItem(LS_REFRESH_KEY)\n },\n }\n}\n\n// ─── 内存存储 ─────────────────────────────────────────────────────────────────\n\n/**\n * 创建内存 Token 存储(Node.js 测试 / SSR / 短生命周期场景)。\n * 页面刷新后丢失。\n *\n * @example\n * ```ts\n * const storage = apiClient.tokenStorage.memory()\n * await storage.setAccessToken('abc')\n * ```\n */\nexport function createMemoryTokenStorage(): TokenStorage {\n let accessToken: string | null = null\n let refreshToken: string | null = null\n return {\n async getAccessToken() { return accessToken },\n async setAccessToken(token) { accessToken = token },\n async getRefreshToken() { return refreshToken },\n async setRefreshToken(token) { refreshToken = token },\n async clear() {\n accessToken = null\n refreshToken = null\n },\n }\n}\n\n// ─── httpOnly Cookie 存储 ────────────────────────────────────────────────────\n\n/**\n * 创建基于 httpOnly cookie 的 Token 存储(生产环境推荐方案)。\n *\n * **安全模型:**\n * - `accessToken` 保存在**内存**中:JS 可读写,用于注入 `Authorization: Bearer`;\n * 页面刷新后丢失,由 refresh 流程自动补充。\n * - `refreshToken` 保存在服务端设置的 **httpOnly cookie** 中:\n * - JS 不可读写,XSS 脚本无法直接盗取。\n * - 结合 `SameSite=Strict` 可阻止 CSRF 攻击。\n * - 浏览器在每次请求时自动携带,无需客户端显式传递。\n *\n * **服务端配套要求:**\n * 1. 登录端点在响应中设置:\n * ```\n * Set-Cookie: refresh_token=<token>; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=<seconds>\n * ```\n * `Path` 限定为 refresh 端点,减少 Cookie 暴露面。\n * 2. Refresh 端点从 Cookie 读取 refresh token(忽略请求体中的 `refreshToken` 字段)。\n * 3. Logout 端点清除 Cookie:\n * ```\n * Set-Cookie: refresh_token=; HttpOnly; SameSite=Strict; Secure;\n * Path=/api/v1/auth/refresh; Max-Age=0\n * ```\n *\n * **跨域注意事项:**\n * 若 API 与前端不同源,服务端还需设置:\n * `Access-Control-Allow-Origin: <前端域名>` 和 `Access-Control-Allow-Credentials: true`。\n * 调用方不需要做额外配置,`apiClient.create` 会自动识别并设置 `credentials: 'include'`。\n *\n * @example\n * ```ts\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { storage: apiClient.tokenStorage.httpOnlyCookie() },\n * })\n * ```\n */\nexport function createHttpOnlyCookieTokenStorage(): TokenStorage {\n // access token 保存在内存中,用于注入 Authorization 请求头\n let memoryAccessToken: string | null = null\n return {\n async getAccessToken() { return memoryAccessToken },\n async setAccessToken(token) { memoryAccessToken = token },\n // refresh token 由服务端通过 Set-Cookie 设置为 httpOnly cookie;\n // JS 不可见,返回哨兵值让 TokenManager 识别此模式。\n async getRefreshToken() { return HTTPONLY_COOKIE_SENTINEL },\n // refresh token 由服务端管理,JS 端无法写入 httpOnly cookie,忽略此调用。\n async setRefreshToken(_token) { /* 由服务端 Set-Cookie 响应头管理,客户端无需操作 */ },\n async clear() {\n memoryAccessToken = null\n // httpOnly cookie 的清除必须由服务端 logout 端点完成(Set-Cookie: Max-Age=0);\n // 客户端无法通过 JS 直接删除 httpOnly cookie。\n },\n }\n}\n\n// ─── Token 管理器 ─────────────────────────────────────────────────────────────\n\n/** 校验 refresh 响应体中的 Token 字段;httpOnly cookie 模式允许不返回 refreshToken。 */\nconst RefreshTokenPayloadSchema = z.object({\n accessToken: z.string().min(1),\n refreshToken: z.string().min(1).optional(),\n expiresIn: z.number().optional(),\n tokenType: z.string().optional(),\n})\n\ntype RefreshCallback = (tokens: TokenPair) => void\n\n/**\n * 创建 Token 管理器:封装存储读写、自动刷新、并发去重与回调通知。\n *\n * 主要供 `apiClient.create` 内部使用,同时作为 named export 暴露给测试与高级用户。\n *\n * 行为约定:\n * - 并发多次 `refresh()` 共享同一个请求。\n * - refresh 接口失败、响应字段缺失或 401,都视为刷新失败:清空存储并触发 `onRefreshFailed`。\n * - 兼容两种响应格式:`{ data: tokens }` 与 `{ data: { tokens } }`(HaiResult 包装)。\n *\n * @example\n * ```ts\n * const manager = createTokenManager(\n * apiClient.tokenStorage.memory(),\n * 'https://api.example.com/api/v1/auth/refresh',\n * fetch,\n * () => notifyLoginRequired(),\n * )\n *\n * const off = manager.onTokenRefreshed(tokens => syncTokens(tokens))\n * const tokens = await manager.refresh()\n * off()\n * ```\n *\n * @param storage - Token 存储适配器\n * @param refreshEndpointUrl - refresh 接口完整 URL(`baseUrl + refreshPath`)\n * @param fetchFn - fetch 实现(允许注入 mock)\n * @param onRefreshFailed - 刷新失败回调\n * @param refreshTimeoutMs - 刷新请求超时(ms),默认 10_000\n * @returns Token 管理器实例\n */\nexport function createTokenManager(\n storage: TokenStorage,\n refreshEndpointUrl: string,\n fetchFn: typeof globalThis.fetch,\n onRefreshFailed?: () => void,\n refreshTimeoutMs: number = 10_000,\n) {\n const callbacks: RefreshCallback[] = []\n let refreshPromise: Promise<TokenPair | null> | null = null\n\n async function refresh(): Promise<TokenPair | null> {\n // 并发去重:同一时刻只发一次 refresh,其它调用复用同一个 Promise\n if (refreshPromise)\n return refreshPromise\n refreshPromise = doRefresh()\n try {\n return await refreshPromise\n }\n finally {\n refreshPromise = null\n }\n }\n\n async function doRefresh(): Promise<TokenPair | null> {\n const refreshToken = await storage.getRefreshToken()\n if (!refreshToken) {\n logger.warn('Token refresh skipped, no refresh token available')\n onRefreshFailed?.()\n return null\n }\n\n logger.debug('Refreshing token')\n // 超时保护:避免 refresh 接口挂死时 dedup Promise 永不 resolve,连带所有等待者 hang。\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), refreshTimeoutMs)\n try {\n // httpOnly cookie 模式:浏览器自动发送 Cookie,请求体无需携带 refreshToken;\n // 同时设置 credentials:'include' 以支持跨域 Cookie 发送。\n const useHttpOnlyCookie = refreshToken === HTTPONLY_COOKIE_SENTINEL\n const requestInit: RequestInit = {\n method: 'POST',\n body: useHttpOnlyCookie ? undefined : JSON.stringify({ refreshToken }),\n credentials: useHttpOnlyCookie ? 'include' : 'same-origin',\n signal: controller.signal,\n }\n if (!useHttpOnlyCookie) {\n requestInit.headers = { 'Content-Type': 'application/json' }\n }\n const response = await fetchFn(refreshEndpointUrl, requestInit)\n\n if (!response.ok) {\n // 仅在服务端明确返回 401/4xx 时才清除 Token(refresh token 已失效);\n // 5xx 作为服务端临时故障,保留 Token 让下一次请求重试。\n if (response.status >= 400 && response.status < 500) {\n await storage.clear()\n onRefreshFailed?.()\n }\n else {\n logger.warn('Token refresh transient failure, keeping tokens', { status: response.status })\n }\n return null\n }\n\n const body = await response.json() as { data?: unknown }\n const parsed = RefreshTokenPayloadSchema.safeParse(readTokenPayload(body.data))\n if (!parsed.success) {\n logger.warn('Token refresh returned invalid data', { issues: parsed.error.issues })\n await storage.clear()\n onRefreshFailed?.()\n return null\n }\n if (!useHttpOnlyCookie && !parsed.data.refreshToken) {\n logger.warn('Token refresh returned invalid data', { issues: [{ path: ['refreshToken'], message: 'Required' }] })\n await storage.clear()\n onRefreshFailed?.()\n return null\n }\n\n // 默认值兜底,避免不安全强转\n const refreshTokenForStorage = parsed.data.refreshToken ?? HTTPONLY_COOKIE_SENTINEL\n const tokens: TokenPair = {\n accessToken: parsed.data.accessToken,\n refreshToken: refreshTokenForStorage,\n expiresIn: parsed.data.expiresIn ?? 3600,\n tokenType: 'Bearer',\n }\n await storage.setAccessToken(tokens.accessToken)\n await storage.setRefreshToken(refreshTokenForStorage)\n logger.info('Token refreshed successfully')\n for (const cb of callbacks) cb(tokens)\n return tokens\n }\n catch (error) {\n // 网络或 fetch 本身抛异常(超时、断网、CORS)视为可重试的暂时性错误,\n // 不清除 Token,仅记录日志。调用方可在下次请求时重试。\n logger.error('Token refresh failed (transient)', { error })\n return null\n }\n finally {\n clearTimeout(timeoutId)\n }\n }\n\n return {\n storage,\n refresh,\n async setTokens(tokens: TokenPair) {\n await storage.setAccessToken(tokens.accessToken)\n if (tokens.refreshToken)\n await storage.setRefreshToken(tokens.refreshToken)\n },\n async clear() {\n await storage.clear()\n },\n onTokenRefreshed(callback: RefreshCallback): () => void {\n callbacks.push(callback)\n return () => {\n const idx = callbacks.indexOf(callback)\n if (idx >= 0)\n callbacks.splice(idx, 1)\n }\n },\n }\n}\n\n/** Token 管理器类型(`createTokenManager` 返回值)。 */\nexport type TokenManager = ReturnType<typeof createTokenManager>\n\n/** 兼容 `{ data: tokens }` 与 `{ data: { tokens } }` 两种 refresh 响应体。 */\nfunction readTokenPayload(data: unknown): unknown {\n if (typeof data === 'object' && data !== null && 'tokens' in data) {\n return (data as { tokens: unknown }).tokens\n }\n return data\n}\n","{\n \"apiClient_networkError\": \"Network request failed\",\n \"apiClient_timeout\": \"Request timeout\",\n \"apiClient_serverError\": \"Server error ({status})\",\n \"apiClient_unauthorized\": \"Unauthorized, please log in again\",\n \"apiClient_forbidden\": \"Access forbidden\",\n \"apiClient_notFound\": \"Resource not found\",\n \"apiClient_validationFailed\": \"Request validation failed\",\n \"apiClient_tokenRefreshFailed\": \"Token refresh failed, please log in again\",\n \"apiClient_notInitialized\": \"API client not initialized, please call apiClient.init() first\",\n \"apiClient_alreadyInitialized\": \"API client is already initialized; call apiClient.close() before init()\",\n \"apiClient_configError\": \"API client config error: {error}\",\n \"apiClient_responseValidationFailed\": \"Response validation failed\",\n \"apiClient_unknown\": \"Unknown error\"\n}\n","{\n \"apiClient_networkError\": \"网络请求失败\",\n \"apiClient_timeout\": \"请求超时\",\n \"apiClient_serverError\": \"服务器错误({status})\",\n \"apiClient_unauthorized\": \"未授权,请重新登录\",\n \"apiClient_forbidden\": \"无权限访问\",\n \"apiClient_notFound\": \"请求的资源不存在\",\n \"apiClient_validationFailed\": \"请求参数校验失败\",\n \"apiClient_tokenRefreshFailed\": \"Token刷新失败,请重新登录\",\n \"apiClient_notInitialized\": \"API 客户端未初始化,请先调用 apiClient.init()\",\n \"apiClient_alreadyInitialized\": \"API 客户端已初始化,请先调用 apiClient.close() 再重新初始化\",\n \"apiClient_configError\": \"API 客户端配置错误:{error}\",\n \"apiClient_responseValidationFailed\": \"响应数据校验失败\",\n \"apiClient_unknown\": \"未知错误\"\n}\n","/**\n * @h-ai/api-client — i18n\n *\n * 模块 i18n 文案访问入口。\n * @module api-client-i18n\n */\n\nimport { core } from '@h-ai/core'\nimport messagesEnUS from '../messages/en-US.json'\nimport messagesZhCN from '../messages/zh-CN.json'\n\ntype ApiClientMessageKey = keyof typeof messagesZhCN\n\n/**\n * api-client 模块 i18n 文案获取器\n *\n * @param key - 文案 Key\n * @param options - 插值参数\n * @returns 本地化后的文案字符串\n */\nexport const apiClientM = core.i18n.createMessageGetter<ApiClientMessageKey>({\n 'zh-CN': messagesZhCN,\n 'en-US': messagesEnUS,\n})\n","/**\n * @h-ai/api-client — 类型与错误码\n *\n * 公共类型(TokenPair、TokenStorage、ApiClientConfig 等)与标准错误集合(HaiApiClientError)。\n * 不包含实现,仅用于跨文件共享与对外导出。\n * @module api-client-types\n */\n\nimport type { ErrorInfo, HaiResult } from '@h-ai/core'\nimport type { CryptoFunctions } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport { core } from '@h-ai/core'\n\n// ─── 错误码 ───────────────────────────────────────────────────────────────────\n\nconst ApiClientErrorInfo = {\n NETWORK_ERROR: '001:502',\n TIMEOUT: '002:504',\n SERVER_ERROR: '003:502',\n UNAUTHORIZED: '004:401',\n FORBIDDEN: '005:403',\n NOT_FOUND: '006:404',\n VALIDATION_FAILED: '007:400',\n TOKEN_REFRESH_FAILED: '008:401',\n NOT_INITIALIZED: '010:500',\n CONFIG_ERROR: '011:500',\n UNKNOWN: '099:500',\n} as const satisfies ErrorInfo\n\n/**\n * API client 标准错误集合(遵循 `@h-ai/core` 错误码规范)。\n *\n * @example\n * ```ts\n * if (!result.success && result.error.code === HaiApiClientError.UNAUTHORIZED.code) {\n * redirectToLogin()\n * }\n * ```\n */\nexport const HaiApiClientError = core.error.buildHaiErrorsDef('api-client', ApiClientErrorInfo)\n\n// ─── Token ────────────────────────────────────────────────────────────────────\n\n/** Token 对(与 `@h-ai/iam` 的 TokenPair 对齐)。 */\nexport interface TokenPair {\n /** 短期访问凭证,请求时作为 `Authorization: Bearer ...`。 */\n readonly accessToken: string\n /** 长期刷新凭证;httpOnly cookie 模式下由服务端管理,响应体中可能不存在。 */\n readonly refreshToken?: string\n /** accessToken 有效期秒数。 */\n readonly expiresIn: number\n /** 固定为 `'Bearer'`。 */\n readonly tokenType: 'Bearer'\n}\n\n/**\n * Token 存储适配器。\n *\n * 内置实现:\n * - `apiClient.tokenStorage.httpOnlyCookie()` — **默认**;浏览器端推荐,refresh token 由服务端管理,防 XSS。\n * - `apiClient.tokenStorage.memory()` — Node.js 测试 / SSR(需显式传入)。\n * - `apiClient.tokenStorage.localStorage()` — 浏览器 SPA / PWA(有 XSS 风险,生产不推荐)。\n *\n * Capacitor / 小程序场景请由对应模块提供(例如 `@h-ai/capacitor`)。\n */\nexport interface TokenStorage {\n readonly getAccessToken: () => Promise<string | null>\n readonly setAccessToken: (token: string) => Promise<void>\n readonly getRefreshToken: () => Promise<string | null>\n readonly setRefreshToken: (token: string) => Promise<void>\n readonly clear: () => Promise<void>\n}\n\n// ─── 配置 ─────────────────────────────────────────────────────────────────────\n\n/** Token 自动刷新配置。 */\nexport interface AuthConfig {\n /**\n * Token 存储适配器;默认 `apiClient.tokenStorage.httpOnlyCookie()`(refresh token 由服务端管理,防 XSS)。\n *\n * - SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n * - 浏览器 SPA 如确需持久化,可显式传 `apiClient.tokenStorage.localStorage()`,\n * 但请先评估 XSS 风险。\n * - Capacitor / 小程序请使用对应平台的安全存储适配器(如 `@h-ai/capacitor`)。\n */\n readonly storage?: TokenStorage\n /** 刷新接口路径,相对于 `baseUrl`,默认 `/auth/refresh`。 */\n readonly refreshPath?: string\n /**\n * 刷新请求超时(ms),默认 10_000。\n * 避免 refresh 接口上游挂死时所有 dedup 调用方一起 hang。\n * 超时后按现有 transient 失败处理(不清 token,下次请求可重试)。\n */\n readonly refreshTimeoutMs?: number\n /** 刷新成功回调,用于同步状态(例如多 Tab 广播)。 */\n readonly onTokenRefreshed?: (tokens: TokenPair) => void\n /** 刷新失败回调(如 refresh token 已失效),通常用于跳转登录页。 */\n readonly onRefreshFailed?: () => void\n}\n\n/** API client 初始化配置。 */\nexport interface ApiClientConfig {\n /** API service 完整 baseUrl,通常含 `apiPrefix`,例如 `https://api.example.com/api/v1`。 */\n readonly baseUrl: string\n /** 认证配置;不传则不启用 Token 注入与自动刷新。 */\n readonly auth?: AuthConfig\n /** 请求超时(ms),默认 30000。 */\n readonly timeout?: number\n /** 客户端标识,写入 `x-request-client` 请求头,默认 `hai-api-client`。 */\n readonly clientName?: string\n /** 额外请求头,可为静态对象或异步函数。 */\n readonly headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>)\n /** 自定义 fetch(测试 mock / Capacitor / 小程序桥接)。 */\n readonly fetch?: typeof globalThis.fetch\n /**\n * 启用端到端传输加密(opt-in)。\n *\n * 配置后所有出站请求会在底层 fetch 之上做一次混合加密,并自动解密\n * 标记为 `X-Encrypted: true` 的响应。服务端需启用对应的 `serv.createApp({ transport })`。\n *\n * `crypto` 字段结构与 `@h-ai/crypto` 的 `crypto` 实例兼容;通常直接传入:\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { crypto } from '@h-ai/crypto'\n *\n * await crypto.init()\n * await apiClient.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * transport: { crypto },\n * })\n * ```\n */\n readonly transport?: ApiClientTransportConfig\n}\n\n// ─── 传输加密 ────────────────────────────────────────────────────────────────\n\n/** {@link ApiClientConfig.transport} 的配置项。 */\nexport interface ApiClientTransportConfig {\n /**\n * crypto 服务实例(通常直接传入 `@h-ai/crypto` 的 `crypto`)。\n * api-client 通过 `crypto.transport.createClient(...)` 创建会话。\n */\n readonly crypto: CryptoFunctions\n /**\n * 密钥协商端点路径,相对于 `baseUrl`,默认 `/_hai/key-exchange`。\n *\n * 必须与服务端 `serv.createApp({ transport: { keyExchangePath } })` 一致。\n */\n readonly keyExchangePath?: string\n}\n\n// ─── Client 接口 ──────────────────────────────────────────────────────────────\n\n/** Token 辅助操作。 */\nexport interface ApiClientAuth {\n /** 手动写入 Token(例如登录成功后)。 */\n readonly setTokens: (tokens: TokenPair) => Promise<void>\n /** 清空存储的 Token(登出)。 */\n readonly clear: () => Promise<void>\n /** 订阅 Token 刷新成功事件,返回取消订阅函数。 */\n readonly onTokenRefreshed: (callback: (tokens: TokenPair) => void) => () => void\n}\n\n/** API client 生命周期能力。 */\nexport interface ApiClientLifecycle {\n /**\n * 初始化 client;必须在调用任何 procedure 前完成。\n *\n * @example\n * ```ts\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * timeout: 15_000,\n * })\n * ```\n */\n readonly init: (config: ApiClientConfig) => Promise<HaiResult<void>>\n /** 释放资源并允许重新 `init`。 */\n readonly close: () => Promise<void>\n /** 当前脱敏后的运行配置快照;未初始化时为 `null`。 */\n readonly config: ApiClientConfig | null\n /** 是否已完成初始化。 */\n readonly isInitialized: boolean\n /** Token 辅助操作。 */\n readonly auth: ApiClientAuth\n}\n\n/** 根据 oRPC contract 推导出的 typed API client(生命周期 + procedure 调用)。 */\nexport type ApiClient<TContract extends AnyContractRouter>\n = ApiClientLifecycle & JsonifiedClient<ContractRouterClient<TContract>>\n","/**\n * @h-ai/api-client — 主实现\n *\n * 包含两部分:\n * - `createApiClient(contract)`:根据 oRPC contract 创建 typed client 工厂(内部实现)。\n * - `apiClient`:统一公开入口;自身就是默认单例,同时挂载 `create` / `tokenStorage`。\n *\n * Client 同时具备生命周期能力(init/close/config/isInitialized/auth)和 contract\n * 推导出的 procedure 嵌套调用能力(如 `client.iam.auth.login(input)`)。\n *\n * 内置能力:\n * - 401 自动 refresh + 单次重试\n * - 并发 refresh 共享 Promise\n * - 请求超时(AbortController)\n * - `ORPCError` → `HaiApiClientError` 标准化映射\n * - 未初始化时调用 procedure 返回 `NOT_INITIALIZED`,不抛异常\n *\n * @example 默认单例\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const me = await apiClient.iam.auth.currentUser()\n * await apiClient.close()\n * ```\n *\n * @example 自定义 contract\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * ```\n *\n * @module api-client-main\n */\n\nimport type { HaiResult } from '@h-ai/core'\nimport type { TransportClient } from '@h-ai/crypto'\nimport type { AnyContractRouter, ContractRouterClient } from '@orpc/contract'\nimport type { JsonifiedClient } from '@orpc/openapi-client'\nimport type { TokenManager } from './api-client-auth.js'\nimport type {\n ApiClient,\n ApiClientAuth,\n ApiClientConfig,\n ApiClientLifecycle,\n} from './api-client-types.js'\nimport { apiContract } from '@h-ai/api-contract'\nimport { core, err, ok } from '@h-ai/core'\nimport { createORPCClient, ORPCError } from '@orpc/client'\nimport { OpenAPILink } from '@orpc/openapi-client/fetch'\nimport {\n createHttpOnlyCookieTokenStorage,\n createLocalStorageTokenStorage,\n createMemoryTokenStorage,\n createTokenManager,\n} from './api-client-auth.js'\nimport { apiClientM } from './api-client-i18n.js'\nimport { HaiApiClientError } from './api-client-types.js'\n\nconst DEFAULT_TIMEOUT = 30_000\nconst DEFAULT_CLIENT_NAME = 'hai-api-client'\nconst DEFAULT_REFRESH_PATH = apiContract.pathOf(apiContract.iam.auth.refresh)\nconst TRAILING_SLASHES_REGEX = /\\/+$/\n\n/** Token 存储方案集合:内存 / 浏览器 localStorage / httpOnly Cookie。 */\nconst tokenStorage = {\n /** 内存存储——SSR / Node 测试或一次性会话使用。 */\n memory: createMemoryTokenStorage,\n /** 浏览器 `localStorage` 存储——单页应用便捷选项;注意 XSS 风险。 */\n localStorage: createLocalStorageTokenStorage,\n /** httpOnly Cookie 存储——浏览器场景推荐方案。 */\n httpOnlyCookie: createHttpOnlyCookieTokenStorage,\n} as const\n\n// ─── 工厂 ─────────────────────────────────────────────────────────────────────\n\n/**\n * 根据 oRPC contract 创建 typed API client。\n *\n * 返回对象同时具备:\n * - **生命周期能力**:`init` / `close` / `config` / `isInitialized` / `auth`\n * - **contract procedure**:按 contract 嵌套结构调用,例如 `client.iam.auth.login(input)`\n *\n * @example 基础用法\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n * import { apiContract } from '@h-ai/api-contract'\n *\n * const client = apiClient.create(apiContract.create({ iam: apiContract.iam }))\n * await client.init({ baseUrl: 'https://api.example.com/api/v1' })\n *\n * const result = await client.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * if (result.success) {\n * await client.auth.setTokens(result.data.tokens)\n * }\n *\n * await client.close()\n * ```\n *\n * @example 自定义 fetch / 超时\n * ```ts\n * const client = apiClient.create(contract)\n * await client.init({\n * baseUrl: 'https://api.example.com/api/v1',\n * fetch: customFetch,\n * timeout: 10_000,\n * })\n * ```\n */\nexport function createApiClient<const TContract extends AnyContractRouter>(\n contract: TContract,\n): ApiClient<TContract> {\n // 客户端运行时状态\n const state: {\n config: ApiClientConfig | null\n rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null\n tokenManager: TokenManager | undefined\n transport: TransportClient | undefined\n } = {\n config: null,\n rawClient: null,\n tokenManager: undefined,\n transport: undefined,\n }\n\n const auth: ApiClientAuth = {\n async setTokens(tokens) { await state.tokenManager?.setTokens(tokens) },\n async clear() { await state.tokenManager?.clear() },\n onTokenRefreshed(callback) {\n return state.tokenManager?.onTokenRefreshed(callback) ?? (() => {})\n },\n }\n\n let initPromise: Promise<HaiResult<void>> | null = null\n const lifecycle: ApiClientLifecycle = {\n async init(config) {\n // 并发重入防护:同时多次 init() 只执行一次。\n // 另一个调用方 await 同一个 in-flight Promise,避免重复创建 raw client / token manager。\n if (initPromise)\n return initPromise\n // 已初始化后必须先 close() 再重新 init,避免旧 transport 会话被新状态覆盖后无法 destroy。\n if (state.rawClient) {\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_alreadyInitialized'),\n )\n }\n initPromise = (async (): Promise<HaiResult<void>> => {\n let nextTransport: TransportClient | undefined\n try {\n nextTransport = buildTransportSession(config)\n const nextTokenManager = buildTokenManager(config, nextTransport)\n const nextRawClient = buildRawClient(contract, config, nextTokenManager, nextTransport)\n\n // 所有可能失败的构建步骤完成后再提交状态,避免调用方看到半初始化 client。\n state.config = config\n state.transport = nextTransport\n state.tokenManager = nextTokenManager\n state.rawClient = nextRawClient\n return ok(undefined)\n }\n catch (error) {\n // 构建过程中如果 transport 已创建但后续失败,必须主动释放,避免会话/定时器泄漏。\n nextTransport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n return err(\n HaiApiClientError.CONFIG_ERROR,\n apiClientM('apiClient_configError', { params: { error: String(error) } }),\n error,\n )\n }\n })()\n try {\n return await initPromise\n }\n finally {\n initPromise = null\n }\n },\n async close() {\n state.transport?.destroy()\n state.config = null\n state.rawClient = null\n state.tokenManager = undefined\n state.transport = undefined\n },\n get config() { return state.config ? core.sanitize.sanitizeSensitiveFields(state.config) : null },\n get isInitialized() { return state.rawClient !== null },\n auth,\n }\n\n /**\n * 顶层 Proxy:把“一个对象同时扮演两种角色”的需求揉到一起。\n *\n * 这个 client 既要像一个普通服务对象,暴露生命周期方法:\n * - `client.init()`\n * - `client.close()`\n * - `client.config`\n * - `client.isInitialized`\n * - `client.auth`\n *\n * 又要像一个按 contract 动态展开的 oRPC client,支持:\n * - `client.iam.auth.login(input)`\n * - `client.storage.object.put(input)`\n * - `client.ai.chat.send(input)`\n *\n * 由于第二类 API 的层级结构是“运行时按属性链逐步访问”的,无法靠写死对象字面量覆盖所有路径,\n * 所以这里使用 Proxy:\n * - 先拦截顶层属性访问;\n * - 如果访问的是生命周期字段,就直接返回 lifecycle 中的真实成员;\n * - 否则把这个属性当成 contract 路径的第一段,继续交给 `createProcedureProxy()` 处理。\n *\n * 换句话说:\n * - `client.init` → 返回 lifecycle.init\n * - `client.auth` → 返回 lifecycle.auth\n * - `client.iam` → 返回一个新的嵌套 Proxy,继续等待 `.auth.login(...)`\n *\n * 注意:如果将来 contract 顶层字段和 lifecycle 字段重名,这里会“优先返回 lifecycle”。\n */\n return new Proxy({}, {\n get(_target, property) {\n // Proxy 的属性键不一定是 string,也可能是 Symbol(例如某些运行时内部探测)。\n // 本 client 只处理字符串路径;非字符串键直接忽略,避免把 Symbol 误当成 API 路径的一部分。\n if (typeof property !== 'string')\n return undefined\n\n // 顶层字段命中 lifecycle 时,直接返回真实成员。\n // 例如:\n // - property === 'init' → 返回 init 函数\n // - property === 'auth' → 返回 auth 对象\n // 这样 `client.init()` / `client.auth.clear()` 就能像普通对象一样工作。\n if (property in lifecycle) {\n return Reflect.get(lifecycle, property)\n }\n\n // 走到这里,说明它不是生命周期字段,那就把它当成 contract 路径的第一段。\n // 例如访问 `client.iam` 时,这里会创建一个 path = ['iam'] 的嵌套 Proxy。\n // 后续如果继续访问 `.auth.login`,path 会逐步变成:\n // ['iam', 'auth'] → ['iam', 'auth', 'login']\n // 最终在函数调用时再映射到真正的 rawClient.iam.auth.login(...args)。\n return createProcedureProxy(state, [property])\n },\n // 这里的返回值在运行时是一个 Proxy,但从“可用能力”上看,它符合 `ApiClient<TContract>`:\n // - 一部分成员来自 lifecycle\n // - 另一部分成员来自 contract 推导出的 procedure 层级\n // 因此使用类型断言把这个动态对象声明为最终对外的 typed client。\n }) as ApiClient<TContract>\n}\n\n// ─── 默认单例 ─────────────────────────────────────────────────────────────────\n\n/**\n * 默认 API typed client:绑定 `iam` / `storage` / `ai` 三个领域。\n *\n * 大多数应用只需 `import { apiClient }` 并调用 `apiClient.init()` 即可。\n *\n * @example\n * ```ts\n * import { apiClient } from '@h-ai/api-client'\n *\n * await apiClient.init({\n * baseUrl: 'http://localhost:3000/api/v1',\n * auth: { refreshPath: '/auth/refresh' },\n * })\n * const result = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })\n * await apiClient.close()\n * ```\n */\nconst defaultApiContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })\n\n/** `apiClient` 默认单例 + 工厂命名空间的统一对外类型。 */\nexport type DefaultApiClient = ApiClient<typeof defaultApiContract> & {\n readonly create: typeof createApiClient\n readonly tokenStorage: typeof tokenStorage\n}\n\n/** hai-framework API client 统一入口(默认单例 + `create` + `tokenStorage`)。 */\nexport const apiClient = new Proxy(createApiClient(defaultApiContract) as ApiClient<typeof defaultApiContract>, {\n get(target, property, receiver) {\n if (property === 'create')\n return createApiClient\n if (property === 'tokenStorage')\n return tokenStorage\n return Reflect.get(target as object, property, receiver)\n },\n}) as DefaultApiClient\n\n// ─── 内部:procedure 代理 ─────────────────────────────────────────────────────\n\n/**\n * 构造嵌套 procedure 代理。\n *\n * oRPC client 接口是任意层嵌套的属性 + 末端调用(如 `client.iam.auth.login(input)`)。\n * 这里用统一的 Proxy 透传所有路径,叶子节点(被 apply 时)映射到\n * `state.rawClient[a][b][c](input)` 并把错误标准化为 `HaiResult`。\n */\nfunction createProcedureProxy<TContract extends AnyContractRouter>(\n state: { rawClient: JsonifiedClient<ContractRouterClient<TContract>> | null },\n path: string[],\n): unknown {\n return new Proxy(() => undefined, {\n get(_t, property) {\n if (typeof property !== 'string')\n return undefined\n return createProcedureProxy(state, [...path, property])\n },\n apply(_t, _thisArg, args) {\n return callProcedure(state.rawClient, path, args)\n },\n })\n}\n\nasync function callProcedure(\n rawClient: unknown,\n path: string[],\n args: unknown[],\n): Promise<unknown> {\n if (!rawClient) {\n return err(HaiApiClientError.NOT_INITIALIZED, apiClientM('apiClient_notInitialized'))\n }\n\n // 沿 path 解析 rawClient[a][b][c]...\n let target: unknown = rawClient\n for (const segment of path) {\n if ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n target = Reflect.get(target as object, segment)\n }\n if (typeof target !== 'function') {\n return err(HaiApiClientError.UNKNOWN, apiClientM('apiClient_unknown'))\n }\n\n try {\n return await (target as (...a: unknown[]) => Promise<unknown>)(...args)\n }\n catch (error) {\n return mapClientError(error)\n }\n}\n\n// ─── 内部:构建 raw client 与 token manager ──────────────────────────────────\n\n/** 根据 `auth` 配置决定是否创建 TokenManager。 */\nfunction buildTokenManager(config: ApiClientConfig, transport: TransportClient | undefined): TokenManager | undefined {\n if (!config.auth)\n return undefined\n\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n // 默认使用 httpOnly cookie 存储(推荐方案:refresh token 由服务端管理,防 XSS)。\n // SSR / Node.js 测试场景请显式传入 `apiClient.tokenStorage.memory()`。\n // 浏览器 localStorage 场景请显式传入 `apiClient.tokenStorage.localStorage()`(有 XSS 风险)。\n const storage = config.auth.storage ?? createHttpOnlyCookieTokenStorage()\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const refreshPath = config.auth.refreshPath ?? DEFAULT_REFRESH_PATH\n const refreshUrl = `${baseUrl}${refreshPath.startsWith('/') ? refreshPath : `/${refreshPath}`}`\n\n const manager = createTokenManager(storage, refreshUrl, fetchFn, config.auth.onRefreshFailed, config.auth.refreshTimeoutMs)\n if (config.auth.onTokenRefreshed) {\n manager.onTokenRefreshed(config.auth.onTokenRefreshed)\n }\n return manager\n}\n\n/** 根据 `transport` 配置创建传输加密会话。 */\nfunction buildTransportSession(config: ApiClientConfig): TransportClient | undefined {\n if (!config.transport)\n return undefined\n const baseFetch = config.fetch ?? globalThis.fetch.bind(globalThis)\n const baseUrl = config.baseUrl.replace(TRAILING_SLASHES_REGEX, '')\n const path = config.transport.keyExchangePath ?? '/_hai/key-exchange'\n const keyExchangeUrl = `${baseUrl}${path.startsWith('/') ? path : `/${path}`}`\n return config.transport.crypto.transport.createClient({ keyExchangeUrl, fetch: baseFetch })\n}\n\n/** 构造 oRPC OpenAPILink + raw client。 */\nfunction buildRawClient<TContract extends AnyContractRouter>(\n contract: TContract,\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): JsonifiedClient<ContractRouterClient<TContract>> {\n const link = new OpenAPILink(contract, {\n url: config.baseUrl,\n headers: async () => buildHeaders(config, tokenManager),\n fetch: async (request, init) => fetchWithRefresh(request, init, config, tokenManager, transport),\n })\n return createORPCClient<JsonifiedClient<ContractRouterClient<TContract>>>(link)\n}\n\n/** 组装请求头:用户配置 + 客户端标识 + Bearer Token。 */\nasync function buildHeaders(\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n): Promise<Record<string, string>> {\n const configured = typeof config.headers === 'function' ? await config.headers() : config.headers ?? {}\n const accessToken = await tokenManager?.storage.getAccessToken()\n return {\n ...configured,\n 'x-request-client': config.clientName ?? DEFAULT_CLIENT_NAME,\n ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),\n }\n}\n\n/** 401 自动 refresh 并重试一次。 */\nasync function fetchWithRefresh(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n tokenManager: TokenManager | undefined,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // Fetch Request 的 body 是一次性可读流;为了支持 401 后重发 POST/PUT 等带 body 的请求,\n // 必须在首次发送前 `clone()` 一份。即使没有 401,clone 后的副本也只会被 GC 回收,无副作用。\n const retryRequest = tokenManager ? request.clone() : undefined\n const response = await fetchWithTimeout(request, init, config, transport)\n if (response.status !== 401 || !tokenManager || !retryRequest)\n return response\n\n const tokens = await tokenManager.refresh()\n if (!tokens)\n return response\n\n const headers = new Headers(retryRequest.headers)\n headers.set('authorization', `Bearer ${tokens.accessToken}`)\n return fetchWithTimeout(new Request(retryRequest, { headers }), init, config, transport)\n}\n\n/** fetch + AbortController 超时控制(联动调用方原始 signal)。 */\nasync function fetchWithTimeout(\n request: Request,\n init: { redirect?: Request['redirect'] },\n config: ApiClientConfig,\n transport: TransportClient | undefined,\n): Promise<Response> {\n // 启用传输加密时,用 encryptedFetch 作为实际 fetch;\n // 它会首次请求前自动完成密钥协商,后续请求复用会话。\n const fetchFn = transport?.encryptedFetch ?? config.fetch ?? globalThis.fetch.bind(globalThis)\n const timeout = config.timeout ?? DEFAULT_TIMEOUT\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n // 保留调用方原始 signal 的中断能力:上游主动 abort 也能控制超时 controller。\n const userSignal = request.signal\n if (userSignal) {\n if (userSignal.aborted) {\n controller.abort()\n }\n else {\n userSignal.addEventListener('abort', () => controller.abort(), { once: true })\n }\n }\n try {\n return await fetchFn(new Request(request, { signal: controller.signal }), init)\n }\n finally {\n clearTimeout(timeoutId)\n }\n}\n\n/** ORPCError / AbortError / 其他异常 → 标准化 HaiResult 错误。 */\nfunction mapClientError(error: unknown) {\n if (error instanceof ORPCError) {\n // 保留 ORPCError 的原始 code / status / data,供上层区分具体业务异常(不仅是 HTTP 状态码)。\n const detail = { orpcCode: error.code, status: error.status, data: error.data }\n if (error.status === 401)\n return err(HaiApiClientError.UNAUTHORIZED, error.message, detail)\n if (error.status === 403)\n return err(HaiApiClientError.FORBIDDEN, error.message, detail)\n if (error.status === 404)\n return err(HaiApiClientError.NOT_FOUND, error.message, detail)\n return err(HaiApiClientError.SERVER_ERROR, error.message, detail)\n }\n if (error instanceof DOMException && error.name === 'AbortError') {\n return err(HaiApiClientError.TIMEOUT, apiClientM('apiClient_timeout'), error)\n }\n return err(HaiApiClientError.NETWORK_ERROR, apiClientM('apiClient_networkError'), error)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/api-client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.43",
|
|
5
5
|
"description": "Hai Framework universal HTTP client with Bearer token and API contract support.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"exports": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@orpc/contract": "^1.14.3",
|
|
25
25
|
"@orpc/openapi-client": "^1.14.3",
|
|
26
26
|
"zod": "^4.4.3",
|
|
27
|
-
"@h-ai/api-contract": "0.1.0-alpha.
|
|
28
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
29
|
-
"@h-ai/crypto": "0.1.0-alpha.
|
|
27
|
+
"@h-ai/api-contract": "0.1.0-alpha.43",
|
|
28
|
+
"@h-ai/core": "0.1.0-alpha.43",
|
|
29
|
+
"@h-ai/crypto": "0.1.0-alpha.43"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^25.9.1",
|