一键导入
hai-serv
使用 @h-ai/serv 将 oRPC contract 挂载为最小 HTTP App 抽象;当需求涉及创建 API 服务、装配 procedure、挂载 OpenAPI 文档、配置健康检查、添加认证/权限 pipeline 包装器或切换 Node/Fetch 运行时适配器时使用。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
使用 @h-ai/serv 将 oRPC contract 挂载为最小 HTTP App 抽象;当需求涉及创建 API 服务、装配 procedure、挂载 OpenAPI 文档、配置健康检查、添加认证/权限 pipeline 包装器或切换 Node/Fetch 运行时适配器时使用。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when: using @h-ai/ai, LLM calls, chat completion, tool calling, function calling, MCP server, streaming, memory management, context compression, summarization, token estimation, RAG, knowledge base, AI client, embeddings, reasoning, rerank, file parsing, speech recognition ASR, speech synthesis TTS, audio, A2A agent-to-agent. 使用 @h-ai/ai 进行 LLM 调用、工具定义、MCP 服务器、流式处理、记忆管理、上下文压缩、知识库、推理引擎、Rerank、文件解析、语音识别与合成、A2A 与会话持久化。
Use when: using @h-ai/ai for LLM calls, tools, MCP, streaming, memory/context, RAG, audio, A2A, or the AI client. 当需求涉及 AI 对话、工具、Audio、会话、知识库或 AI 客户端时使用。
Use when: creating or extending apps in hai-framework, adding routes, pages, API endpoints, service workspaces, mobile app, H5 app, admin console. 在 hai-framework 中创建或扩展应用或 API service workspace,包含路由、API 端点、typed contract、服务层与 UI 脚手架代码。
Use when: creating a new module, new package, scaffold, add sub-feature, add provider, create repository, module structure, tsup config, error codes, NotInitializedKit pattern. 在 hai-framework 中创建新模块(package)。
Use when: reviewing app code in hai-framework, auditing app quality, checking app conventions, reviewing routes, reviewing API service workspaces, app security, app i18n review. 对 hai-framework 应用层代码进行审查:路由安全 → 认证授权 → i18n → 组件使用 → API 端点 / service workspace → 服务层 → 性能。
Use when: reviewing code, code review, auditing module quality, checking hai-framework conventions, verifying HaiResult<T> usage, reviewing module structure, PR review, checking naming consistency, verifying NotInitializedKit pattern, auditing performance, security, distributed systems. 对 hai-framework 模块进行全维度代码审查:架构 → 命名 → 类型 → 注释 → 性能 → 分布式 → 安全 → 日志 → 测试 → 文档。
| name | hai-serv |
| description | 使用 @h-ai/serv 将 oRPC contract 挂载为最小 HTTP App 抽象;当需求涉及创建 API 服务、装配 procedure、挂载 OpenAPI 文档、配置健康检查、添加认证/权限 pipeline 包装器或切换 Node/Fetch 运行时适配器时使用。 |
| 项目 | 契约 |
|---|---|
| 能力 | 使用 @h-ai/serv 将 oRPC contract 挂载为最小 HTTP App 抽象;当需求涉及创建 API 服务、装配 procedure、挂载 OpenAPI 文档、配置健康检查、添加认证/权限 pipeline 包装器或切换 Node/Fetch 运行时适配器时使用。 |
| 适用场景 | 当任务与 hai-serv 的能力描述匹配,并且需要遵循本 Skill 的流程和边界时 |
| 输入 | 模块配置、类型化业务参数、依赖初始化状态和目标运行环境 |
| 输出 | 符合模块公共 API 的实现或示例;业务结果使用 HaiResult,并同步必要测试与文档 |
| 限制 | 遵守 init → use → close 生命周期与运行环境边界;不绕过类型、授权、输入校验或敏感信息保护 |
@h-ai/serv是 hai-framework 的 API Service 运行时,对外暴露最小 HTTP App 抽象,将@h-ai/api-contract的领域 contract 挂载成跨端可访问的 HTTP API;Hono 是内部实现细节,应用代码不要直接 import 或暴露 Hono。
⚠️ 服务端模块(Node.js / Fetch Runtime)。 浏览器端通过
@h-ai/api-client调用由本模块暴露的 HTTP API。
生命周期说明:
@h-ai/serv是无状态 HTTP App 装配器,不提供init()/close();依赖模块(iam/storage/ai/crypto 等)由应用先初始化,并在serv.listen(..., { onClose })中反向释放。serv.createApp()的启动期配置错误会 fail-fast 抛出。
@h-ai/api-contract contract 装配成最小 HTTP App 抽象/health、/ready、/openapi.json、/docs 等系统端点requireAuth)或权限(requirePermission)检查@hono/node-server) 或 Fetch Runtime(Cloudflare Workers / Deno)部署ServContext(注入 session、tenant 等请求级数据)config/_serv.yml 管理 API 前缀、OpenAPI、docs、health、rpc 等 HTTP 挂载配置serv.createApp({ transport: { crypto } })createApp({ middlewares }) 自定义 HTTP middleware,或通过共享类型自定义 procedure wrapper// @h-ai/serv 依赖 @h-ai/api-contract 提供 contract 定义
// 业务 procedure 依赖对应领域模块(iam/storage/ai)
import { ai } from '@h-ai/ai'
import { apiContract } from '@h-ai/api-contract'
import { iam } from '@h-ai/iam'
import { serv } from '@h-ai/serv'
import { createAiProcedures } from '@h-ai/serv/features/ai'
import { createIamProcedures } from '@h-ai/serv/features/iam'
import { createStorageProcedures } from '@h-ai/serv/features/storage'
import { storage } from '@h-ai/storage'
const contract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })
// 装配 procedures(每个 feature 对应 contract 中的一个领域)
const procedures = {
iam: createIamProcedures({ iam }),
storage: createStorageProcedures({ storage }),
ai: createAiProcedures({ ai }),
}
// 创建 HTTP App 抽象;不要在应用导出类型里暴露 Hono
const app = serv.createApp({
contract,
procedures,
http: {
apiPrefix: '/api/v1',
openapi: { path: '/openapi.json' },
docs: { path: '/docs' },
health: { path: '/health', readyPath: '/ready' },
},
})
// Node.js 启动(自动读取 PORT / HOST 环境变量;onClose 自动监听 SIGINT/SIGTERM)
serv.listen(app, {
onListening: info => logger.info('API service listening', { port: info.port }),
onClose: closeApp, // 业务模块关闭函数(ai/storage/iam/reldb 等反向释放)
})
const app = serv.createApp({
contract,
procedures,
iam,
audio: {
ai,
verifyTicket: consumeAudioTicket, // 校验用途/时效并原子消费,返回 { session, grant? }(推荐 iam.ticket.consume)
authorize: (session, request, grant) => authorizeAudioRequest(session, request, grant),
onSessionEnd: (session, request) => releaseAudioConcurrency(session.userId, request.operation),
},
})
iam.ticket.issue),短期有效且只能消费一次;连接建立即鉴权(独立预鉴权超时,默认 5s),未鉴权不处理任何业务帧;普通 IAM access token 禁止进入 WebSocket URL。verifyTicket 返回 HaiResult<AudioTicketVerification>({ session, grant? });grant 承载票据绑定的操作/模型/会话,供 authorize 交叉校验 start 未越权。start/done 各一次、segmentId 会话内唯一);输入队列、消息积压、发送缓冲均有上限(preAuthTimeoutMs/maxPendingMessages/maxSendBufferBytes 等可调),超限或取消时以领域错误关闭并级联中止上游。authorize 返回的 AuthorizedAudioRequest 是唯一会传给 ai.audio 的模型、音色与格式配置;未提供时客户端的付费参数会被忽略(若票据 grant.model 存在则采用之)。apiContract.create({...}) 接受任意 key——除了框架提供的 iam/storage/ai,应用可以挂入自己的领域 contract,
客户端通过 client.<key>.<procedure>(...) 调用,类型完全推导自同一份 contract。
// apps/api-service-contract/src/app-contract.ts
import { apiContract } from '@h-ai/api-contract'
import { z } from 'zod'
const InfoOutput = apiContract.haiResultSchema(z.object({ name: z.string(), version: z.string() }))
const EchoInput = z.object({ message: z.string().min(1).max(2000) })
const EchoOutput = apiContract.haiResultSchema(z.object({ message: z.string(), userId: z.string() }))
export const appContract = {
info: apiContract.route({ method: 'POST', path: '/app/info', operationId: 'app.info', tags: ['app'] })
.output(InfoOutput),
echo: apiContract.route({ method: 'POST', path: '/app/echo', operationId: 'app.echo', tags: ['app'] })
.input(EchoInput).output(EchoOutput),
}
// src/server/procedures/app-procedures.ts
import type { ServContext } from '@h-ai/serv'
import { appContract } from '@h-ai/api-service-contract'
import { ok } from '@h-ai/core'
import { serv } from '@h-ai/serv'
export function createAppProcedures(deps: { name: string; version: string }) {
const p = serv.implement(appContract).$context<ServContext>()
return p.router({
// 公开 procedure:handler 直接返回 HaiResult
info: p.info.handler(() => ok({ name: deps.name, version: deps.version })),
// 鉴权 procedure:用 serv.requireAuth 包一层,未登录时短路返回 ApiUnauthorized
echo: p.echo.handler(serv.requireAuth(({ input, context }) => ok({
message: input.message,
userId: context.session!.userId,
}))),
})
}
把自有 contract / procedures 与框架的 iam/storage/ai 平铺合并:
const contract = apiContract.create({
iam: apiContract.iam,
storage: apiContract.storage,
ai: apiContract.ai,
app: appContract, // ← 自有契约
})
const procedures = {
iam: createIamProcedures({ iam }),
storage: createStorageProcedures({ storage }),
ai: createAiProcedures({ ai }),
app: createAppProcedures({ name: 'demo', version: '1.0.0' }),
}
const app = serv.createApp({ contract, procedures, http, iam })
注意要点:
client.app.info()。serv.requireAuth 仅可在过程内调用:必须在 context 中有 session;公开过程不要包装它。@h-ai/api-service-contract),禁止从 app 源码目录跨应用 import。import { serv } from '@h-ai/serv'
// toFetch 返回标准 Fetch handler
const handler = serv.toFetch(app)
export default { fetch: handler }
config/_serv.yml 管理 HTTP 配置@h-ai/serv 不自己扫描 YAML;推荐由 @h-ai/core 统一加载,再用 ServConfigSchema 校验:
import { core } from '@h-ai/core'
import { serv, ServConfigSchema } from '@h-ai/serv'
core.init({ configDir: './config' })
const validation = core.config.validate('serv', ServConfigSchema)
if (!validation.success)
throw new Error(validation.error.message)
const servConfig = core.config.getOrThrow<import('@h-ai/serv').ServConfig>('serv')
const app = serv.createApp({
contract,
procedures,
http: servConfig.http,
transport: servConfig.transport === false
? undefined
: {
crypto,
keyExchangePath: servConfig.transport.keyExchangePath,
excludePaths: [...servConfig.transport.excludePaths],
maxClients: servConfig.transport.maxClients,
},
})
# config/_serv.yml
http:
apiPrefix: /api/v1
openapi:
path: /openapi.json
docs:
path: /docs
health:
path: /health
readyPath: /ready
rpc: false
transport:
keyExchangePath: /_hai/key-exchange
excludePaths:
- /health
- /ready
- /openapi.json
- /docs
- /_hai/scalar.js
maxClients: 10000
serv.createApp(options) — 创建 HTTP App 抽象import type { CreateServAppOptions } from '@h-ai/serv'
export function createServerApp() {
return serv.createApp({
contract, // AnyContractRouter — 通过 apiContract.create() 组合的 contract
procedures, // Router<AnyContractRouter, ServContext> — procedure 实现
http?, // ServHttpConfigInput — HTTP 端点配置(见下方配置节)
middlewares?, // readonly ServMiddlewareMount[] — 自定义 HTTP middleware(日志/CORS/限流/租户头校验)
iam?, // ServIam — 顶层 IAM 句柄,同时驱动 access token 校验与 refresh cookie【推荐】
refreshCookie?, // RefreshCookieConfig — httpOnly refresh cookie 刷新路径(见下方 cookie 节)
transport?, // { crypto, keyExchangePath?, excludePaths?, maxClients? } — 统一传输加密
verifyToken?, // (token) => Promise<HaiResult<ServSession>> — 逃脱口:不使用 iam 时提供自定义校验
createContext?, // CreateServContext — 高级:完全接管上下文构造(设置后 serv 不再自动填充 session)
})
}
apiContract.route / serv.implement — contract 与运行时分层应用代码不要直接 import { oc } from '@orpc/contract' 或 import { implement } from '@orpc/server'。
contract 路由统一用 @h-ai/api-contract 定义,procedure 运行时统一用 @h-ai/serv 实现:
import { apiContract } from '@h-ai/api-contract'
import { serv } from '@h-ai/serv'
// 等价于 oc.route(...),但不让应用感知 @orpc/contract
const route = apiContract.route({ method: 'POST', path: '/x', operationId: 'x', tags: ['x'] })
// 等价于 implement(contract)
const p = serv.implement(appContract).$context<ServContext>()
serv 不暴露本地传输加密工厂;内部统一调用 crypto.transport.createServer()。客户端用 @h-ai/api-client 的 transport 配置自动协商。
import { crypto } from '@h-ai/crypto'
import { serv } from '@h-ai/serv'
await crypto.init()
const app = serv.createApp({
contract,
procedures,
http: { apiPrefix: '/api/v1' },
transport: {
crypto,
// keyExchangePath 默认 '/_hai/key-exchange'
// maxClients: 10000,
// keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
},
})
客户端:
await apiClient.init({
baseUrl: 'https://api.example.com/api/v1',
transport: { crypto },
})
若服务端通过 config/_serv.yml 自定义了 transport.keyExchangePath,客户端也必须传入同一路径:
await apiClient.init({
baseUrl: 'https://api.example.com/api/v1',
transport: {
crypto,
keyExchangePath: '/custom/key-exchange',
},
})
不要导入或暴露子目录内部 transport 工厂;公共装配点只有
serv.createApp({ transport: { crypto } })。
多节点部署时,直接在 serv.createApp({ transport }) 的运行时对象里注入 keyStore 即可;推荐从 @h-ai/crypto 根入口导入 createRedisTransportKeyStore() / createReldbTransportKeyStore()。keyStore 不属于 _serv.yml 配置项,配置文件仍只保留静态路径和白名单。
上下文工厂优先级(context.session 填充来源):
createContext(高级场景:多租户、额外字段)verifyToken(逃脱口:使用非 @h-ai/iam 的认证服务)iam.session.verifyToken(推荐:传入 iam 后自动启用)parseRequestContext:仅解析请求元数据,session 为 undefined⚠️
verifyToken会在每次请求调用(不缓存);verifyToken 失败或抛错统一收敛为session=undefined,由requireAuth统一返回 401。
serv.xxx 扁平 API)| 函数 | 说明 |
|---|---|
serv.mapHaiError(handler) | 捕获未处理异常,转换为 HaiResult |
serv.requireAuth(handler) | 验证 session(context.session 非空,即 token 已通过 verifyToken 校验) |
serv.requirePermission(permission, handler) | 验证权限码,支持通配符 serv.WILDCARD_PERMISSION('*') |
serv.requireRole(role, handler) | 验证角色,支持通配符 serv.WILDCARD_ROLE('*') |
serv.validateInputOrFail(zodSchema, input, locale) | 在 procedure 内执行 Zod 二次校验,失败时返回本地化 HaiResult + ValidationFormError[] |
serv.resolveRequestLocale(headers) | 从 x-hai-locale / Accept-Language 解析并规范化 locale |
serv.m(key, { locale, params }) | 读取 serv 自身 i18n 消息(请求级本地化) |
import { serv } from '@h-ai/serv'
// 组合包装(从外到内:error → auth → permission/role → handler)
const handler = serv.mapHaiError(
serv.requireAuth(
serv.requirePermission('user.write', actualHandler)
)
)
// 角色检查示例
const adminOnly = serv.requireAuth(
serv.requireRole('admin', actualHandler)
)
@h-ai/serv 的 pipeline 分三层:
serv.createApp({ middlewares }) 注入 HTTP middlewareverifyToken / createContext / serv.buildAuthContextFactory() 自定义请求上下文ServProcedureWrapper / ServGuardedProcedureWrapper 自定义业务包装器适用于请求日志、trace、限流、CORS、租户头校验等 HTTP 层 横切逻辑。常见 CORS 场景可直接复用 serv.cors(...):
import type { ServMiddleware } from '@h-ai/serv'
import { serv } from '@h-ai/serv'
const requestMetrics: ServMiddleware = async (c, next) => {
const startedAt = Date.now()
await next()
c.header('x-response-time-ms', String(Date.now() - startedAt))
}
const app = serv.createApp({
contract,
procedures,
http: { apiPrefix: '/api/v1' },
middlewares: [
{
middleware: serv.cors({
origin: origin => origin === 'https://app.example.com',
credentials: true,
exposedHeaders: ['X-Encrypted', 'X-Request-Id'],
}),
},
{ middleware: requestMetrics },
{
path: '/api/v1/*',
middleware: async (c, next) => {
if (!c.req.header('x-tenant-id'))
return c.text('Missing x-tenant-id', 400)
await next()
},
},
],
})
执行顺序固定为:securityHeaders → middlewares → transport(若启用)→ health/refresh-cookie/OpenAPI/RPC/docs/oRPC routes。
middlewares 先于 transport 执行,因此 CORS 这类 preflight middleware 可以直接短路返回;若需要读取解密后的业务 body,请改用 context / procedure 层扩展。X-Encrypted),记得通过 serv.cors({ exposedHeaders: [...] }) 显式暴露。如果想复用“Bearer token → session”这段逻辑,再追加租户/工作区等字段,优先:
const baseContext = serv.buildAuthContextFactory(token => iam.session.verifyToken(token))
const app = serv.createApp({
contract,
procedures,
createContext: async ({ request }) => {
const context = await baseContext({ request })
return {
...context,
tenantId: request.headers.get('x-tenant-id') ?? null,
}
},
})
选择建议:
verifyTokenbuildAuthContextFactory(...) + createContextcreateContext适用于已进入 procedure 后的审计、租户约束、业务 guard:
import type { ServGuardedProcedureWrapper, ServProcedureWrapper } from '@h-ai/serv'
import { err, HaiCommonError } from '@h-ai/core'
import { serv } from '@h-ai/serv'
const withAudit: ServProcedureWrapper = handler => async (options) => {
options.context.logger.info('procedure.start', { requestId: options.context.requestId })
return await handler(options)
}
const requireTenant: ServGuardedProcedureWrapper<string> = (tenantId, handler) => async (options) => {
if (options.context.request.headers.get('x-tenant-id') !== tenantId) {
return err(
HaiCommonError.FORBIDDEN,
serv.m('serv_errorForbidden', { locale: options.context.locale }),
)
}
return await handler(options)
}
const createWidget = serv.mapHaiError(
serv.requireAuth(
withAudit(
requireTenant('tenant-a', async ({ input }) => widgetService.create(input)),
),
),
)
说明:
ServMiddleware / ServMiddlewareMount / ServProcedureWrapper / ServGuardedProcedureWrapper 等共享类型由根入口 @h-ai/serv 对外暴露src/pipelines/* 是模块内部默认实现目录,不作为公开子路径 API 文档承诺serv.requireAuth / requirePermission / requireRole / mapHaiError按三层处理:
serv.createApp() 会把默认英文 Zod 错误改写为本地化 errors[]。serv.validateInputOrFail(zodSchema, input, context.locale);简单业务规则直接 err(...) 即可。err(...),并在创建错误消息的那一层用对应模块的 i18n getter 按 context.locale 出消息。import { err, HaiCommonError } from '@h-ai/core'
import { z } from 'zod'
import { serv } from '@h-ai/serv'
const Schema = z.object({ title: z.string().min(1) })
const handler = serv.mapHaiError(async ({ input, context }) => {
const validated = serv.validateInputOrFail(Schema, input, context.locale)
if (!validated.success)
return validated
if (!context.session)
return err(HaiCommonError.UNAUTHORIZED, serv.m('serv_errorUnauthorized', { locale: context.locale }))
return widgetService.create(validated.data)
})
如果错误来自下游模块(如
iam/storage)并且该模块已经返回HaiResult,serv 默认透传它的error.message。由于HaiError目前不携带messageKey/params,serv 边界不能对任意下游错误再做通用重翻译;要做请求级 i18n,必须在创建该错误的模块/feature 里就拿到 locale。
serv.parseRequestContext — 默认 Context 解析器从 HTTP 请求头自动解析:
| 字段 | 来源 |
|---|---|
accessToken | Authorization: Bearer <token> |
requestId | x-request-id 或自动生成 UUID |
ip | x-forwarded-for / x-real-ip |
locale | accept-language |
userAgent | user-agent |
serv.generateSpec(contract, options) — 生成 OpenAPI 文档const spec = await serv.generateSpec(contract, {
title: 'My API',
version: '1.0.0',
apiPrefix: '/api/v1',
description: '接口说明',
})
// spec 为 OpenAPI 3.1 Document 对象
ServHttpConfigInput)| 字段 | 默认值 | 说明 |
|---|---|---|
apiPrefix | '/api/v1' | oRPC OpenAPI handler 挂载前缀 |
health | { path: '/health', readyPath: '/ready' } | 健康与就绪检查端点 |
openapi | false | OpenAPI JSON endpoint,显式开启 |
docs | false | Scalar 文档页,显式开启;启用后自动挂载 /_hai/scalar.js 本地脚本路由 |
rpc | false | 内部 RPC endpoint,显式开启;loopback / private-network 基于真实连接 IP,网关场景用 gateway-only |
transport | undefined | 顶层配置;启用后默认挂载 {apiPrefix}/_hai/key-exchange |
http: {
apiPrefix: '/api/v1',
openapi: { path: '/openapi.json' },
docs: { path: '/docs' },
health: { path: '/health', readyPath: '/ready' },
rpc: false, // 关闭内部 RPC
}
@h-ai/serv 内置了三个 feature 模块,开箱即用:
| 导入路径 | 工厂函数 | 所需依赖 |
|---|---|---|
@h-ai/serv/features/iam | createIamProcedures({ iam }) | @h-ai/iam |
@h-ai/serv/features/storage | createStorageProcedures({ storage }) | @h-ai/storage |
@h-ai/serv/features/ai | createAiProcedures({ ai }) | @h-ai/ai |
import type { ServContext } from '@h-ai/serv'
import { serv } from '@h-ai/serv'
import { myContract } from './my-contract.js'
const { requireAuth, requirePermission } = serv
export function createMyProcedures() {
return {
widget: {
list: requireAuth(async ({ input, context }) => {
// input 类型来自 contract 定义
return ok(await widgetService.list(input))
}),
create: requirePermission('widget:write', async ({ input }) => {
return ok(await widgetService.create(input))
}),
},
}
}
import { apiContract } from '@h-ai/api-contract'
import { widgetContract } from './widget-contract.js'
export const myAppContract = apiContract.create({
iam: apiContract.iam,
widget: widgetContract,
})
const app = serv.createApp({
contract: myAppContract,
procedures: {
iam: createIamProcedures({ iam }),
widget: createMyProcedures(),
},
http: { apiPrefix: '/api/v1' },
})
procedure 包装器内置以下错误:
| 错误码 | 触发条件 |
|---|---|
HaiCommonError.UNAUTHORIZED | requireAuth:context.session 为空(无 token 或 token 校验失败) |
HaiCommonError.FORBIDDEN | requirePermission:无对应权限 |
HaiCommonError.INTERNAL_ERROR | mapHaiError:procedure 抛出未处理异常 |
将 refresh token 存储在 HttpOnly cookie 中,避免 XSS 风险(浏览器 JS 无法读取)。
先区分两个 token:
accessToken:短期凭证,客户端放在内存,通过 Authorization: Bearer <token> 发送;
serv.parseRequestContext() / extractBearerToken() 解析、buildAuthContextFactory() 校验的就是它。refreshToken:长期凭证,启用 refreshCookie 后只保存在 httpOnly cookie;
它不会走 Authorization,也不会被 extractBearerToken() 读取,只会在 /auth/refresh 被服务端取出。@h-ai/api-client 的 apiClient.tokenStorage.httpOnlyCookie()/auth/login → serv 拦截 oRPC 成功响应 → Set-Cookie: hai_refresh_token=...;HttpOnly;SameSite=Strict,并从 JSON 响应体擦除 refreshToken/auth/refresh(浏览器自动携带 cookie)iam.session.refresh → 返回新 access token + 更新 cookie(响应体不暴露 refresh token)/auth/logout → serv 清除 cookie(Max-Age=0)若同时启用 transport: { crypto },/auth/refresh 也必须走加密链路;cookie-only 刷新请求允许空 body,服务端不要把 Request.body === null 误判为需要解密的请求体。
const app = serv.createApp({
contract,
procedures,
http: { apiPrefix: '/api/v1' },
iam, // 顶层句柄:同时驱动 verifyToken 与 refresh
refreshCookie: {
// cookieName?: string 默认 'hai_refresh_token'
// maxAge?: number 默认 30 * 24 * 3600(30 天)
// secure?: boolean 默认 NODE_ENV=production 时开启
// onRefresh?: (token) => ... 可选:覆盖默认的 iam.session.refresh
},
// ✅ 顶层 iam 同时为 access token 校验提供默认实现,无需再传 verifyToken
})
逃脱口:不使用
@h-ai/iam时,可显式传入顶层verifyToken: token => myAuthService.verify(token)。
| 属性 | 值 |
|---|---|
| Name | hai_refresh_token(可配置) |
| Path | {apiPrefix}/auth/refresh(最小范围) |
| HttpOnly | ✓ |
| SameSite | Strict |
| Secure | 生产自动开启,secure: false 可在 HTTP 开发环境关闭 |
| Max-Age | 30 天(可配置) |
pnpm --filter @h-ai/serv test
pnpm --filter @h-ai/serv typecheck
pnpm --filter @h-ai/serv lint