{"_id":"@aipt/api-route","_rev":"2-ff5fd0e7ae4af7c91dc61c83de73d7ba","name":"@aipt/api-route","dist-tags":{"latest":"0.4.4"},"versions":{"0.4.3":{"name":"@aipt/api-route","version":"0.4.3","_id":"@aipt/api-route@0.4.3","maintainers":[{"name":"dyytojerry","email":"zhangyunfeng.jerry@gmail.com"}],"bin":{"dyyto-api":"./dist/bin.js"},"dist":{"shasum":"b578de397b2b63adc6b95d996cc57451c7a8fd50","tarball":"https://registry.npmjs.org/@aipt/api-route/-/api-route-0.4.3.tgz","fileCount":14,"integrity":"sha512-FHdkd1G/b5HATVPR5dY6/2REj5CFhkrZXKlnQOdoxhGXTKOnK3mwdWhIVzwVV47Oz24Pb0D27iaDxGEIj5R1ag==","signatures":[{"sig":"MEQCICzfEpNB2U1esxYSvUksVQtRKgwbL92KD8tT7XfMA6FEAiBHu2VqbLGKDaFJIoWQF5XWTCfokMYZGuRapI8myrramg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":44846},"type":"module","engines":{"node":">=24.18.0 <25"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./cli":{"types":"./dist/cli.d.ts","default":"./dist/cli.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json && chmod +x dist/bin.js","typecheck":"tsc --noEmit -p tsconfig.json"},"_npmUser":{"name":"dyytojerry","email":"zhangyunfeng.jerry@gmail.com"},"description":"基于 Hono 与 Zod 的类型安全 API Route 适配。","directories":{},"_nodeVersion":"24.18.0","dependencies":{"hono":"4.13.3","@hono/zod-openapi":"1.6.1","@aipt/api-contract":"0.2.3"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"zod":"4.4.3","vitest":"4.1.10","typescript":"6.0.3","@types/node":"24.10.13","@aipt/testkit":"0.1.0"},"peerDependencies":{"zod":"^4.0.0"},"_npmOperationalInternal":{"tmp":"tmp/api-route_0.4.3_1788778642172_0.696079493165882","host":"s3://npm-registry-packages-npm-production"}},"0.4.4":{"bin":{"dyyto-api":"./dist/bin.js"},"dependencies":{"@aipt/api-contract":"0.2.4","@hono/zod-openapi":"1.6.1","hono":"4.13.3"},"description":"基于 Hono 与 Zod 的类型安全 API Route 适配。","devDependencies":{"@aipt/testkit":"0.1.2","@types/node":"24.10.13","typescript":"6.0.3","vitest":"4.1.10","zod":"4.4.3"},"engines":{"node":">=24.18.0 <25"},"exports":{".":{"default":"./dist/index.js","types":"./dist/index.d.ts"},"./cli":{"default":"./dist/cli.js","types":"./dist/cli.d.ts"}},"name":"@aipt/api-route","peerDependencies":{"zod":"^4.0.0"},"publishConfig":{"access":"restricted"},"scripts":{"build":"tsc -p tsconfig.build.json && chmod +x dist/bin.js","test":"vitest run","typecheck":"tsc --noEmit -p tsconfig.json"},"type":"module","version":"0.4.4","_nodeVersion":"24.18.0","_id":"@aipt/api-route@0.4.4","dist":{"integrity":"sha512-hMXgEmopNTcTqm7QtuO86QWYzC2L3yup0jojUUJt337PmjAaHvevnMquhbrF8UXzLGD9TN/F+e3zLq8nZxN50A==","shasum":"80cffd05d14f321526618f2955a2f49953df2dd0","tarball":"https://registry.npmjs.org/@aipt/api-route/-/api-route-0.4.4.tgz","fileCount":14,"unpackedSize":44851,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCNMikkSbqXIUkzNepHOrNMygTHnbIVh2RFV7iFgabRjwIhANytscR8x+ptJ81ZQvHJo36FEVfM2knqVLkliZ0Y09uZ"}]},"_npmUser":{"name":"dyytojerry","email":"zhangyunfeng.jerry@gmail.com"},"directories":{},"maintainers":[{"name":"dyytojerry","email":"zhangyunfeng.jerry@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/api-route_0.4.4_1788830061013_0.10804062243516688"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-07T10:57:21.964Z","modified":"2026-09-08T01:14:21.312Z","0.4.3":"2026-09-07T10:57:22.314Z","0.4.4":"2026-09-08T01:14:21.148Z"},"description":"基于 Hono 与 Zod 的类型安全 API Route 适配。","maintainers":[{"name":"dyytojerry","email":"zhangyunfeng.jerry@gmail.com"}],"readme":"# API Route\n\n`@aipt/api-route@0.4.4` 让**同一份路由定义**同时驱动运行时处理器与 OpenAPI 3.1 文档。契约是路由的副产物，不手写，因此不可能与实现漂移。\n\n本包**已内含 HTTP 服务能力**（依赖 `hono` 与 `@hono/zod-openapi`），因此平台不再单独提供「HTTP 服务」代码服务——那只会制造一条能绕过契约的路由方式。\n\n## 安装与导入\n\n```bash\npnpm add @aipt/api-route@0.4.4\n```\n\n```ts\nimport { ApiRouteFailure, createApiRouter, defineRoute } from \"@aipt/api-route\";\n```\n\n## 最小用法\n\n```ts\nimport { z } from \"zod\";\n\nconst readAsset = defineRoute({\n  operationId: \"assets.read\",\n  method: \"get\",\n  path: \"/api/assets/{assetId}\",\n  summary: \"读取资产\",\n  params: z.object({ assetId: z.string().min(1) }),\n  response: z.object({ asset: z.object({ assetId: z.string(), title: z.string() }) }),\n  errors: [{ status: 404, code: \"ASSET_NOT_FOUND\", description: \"资产不存在\" }],\n});\n\nconst router = createApiRouter({\n  title: \"资产 API\", version: \"1.0.0\",\n  routes: { readAsset },\n  publishContractDigest: true,\n  handlers: {\n    readAsset: ({ params }) => {\n      const asset = store.get(params.assetId);\n      if (!asset) throw new ApiRouteFailure(\"ASSET_NOT_FOUND\", \"资产不存在。\", 404);\n      return { asset };\n    },\n  },\n});\n\nrouter.fetch(request);          // 运行时\nrouter.openApiDocument();       // 同一份定义导出的 OpenAPI 3.1\nawait router.contractDigest();  // 供客户端比对的契约摘要\n```\n\n成功载荷自动包进平台信封 `{ ok: true, ...载荷 }`；`ApiRouteFailure` 包进 `{ ok: false, error: { code, message } }` 并带上声明的状态码。\n\n### 异步任务与创建\n\n```ts\ndefineRoute({ /* … */ method: \"post\", successStatus: 202, response: z.object({ task: taskView }) });\n```\n\n`201` 表示已创建，`202` 表示已受理。`GET`/`DELETE` 用它们会在装配期被拒绝。\n\n### 迁移既有 API\n\n把手写路由迁进来时，响应 schema 写错会让路由从返回数据变成返回 500。用 `observe` 档位先暴露缺口：\n\n```ts\ncreateApiRouter({ /* … */ responseValidation: \"observe\", onDefect: (defect) => report(defect) });\n```\n\n违约会上报 `onDefect` 但**仍然放行**，直到缺陷清零再切回默认的 `strict`。\n\n## 命令行 `dyyto-api`\n\n契约随路由每次迭代重算，因此它是**动态约束**——由命令而非一次性注入的胶水代码承担。四个子命令分成两条流水线：\n\n```bash\n# 服务端：改完路由 → 重新导出 → 重新生成客户端 → 封存产物\ndyyto-api emit  --routes ./dist/api-routes.js --out ./openapi.json --export workbenchOpenApiDocument\nopenapi-ts && dyyto-api seal --dir ./src/generated\n\n# 服务端 CI 门禁：路由改了但契约没重新导出，在这里非零退出\ndyyto-api check --routes ./dist/api-routes.js --spec ./openapi.json --export workbenchOpenApiDocument\n\n# 前端 CI 门禁：只持有 openapi.json 的仓库用它，不需要路由模块\ndyyto-api digest --spec ./openapi.json --expect sha256:…\n```\n\n`check` 要动态 import 路由模块，只有服务端有；纯前端仓库拿不到路由，用 `digest` 比对摘要。两端摘要都出自 `@aipt/api-contract` 的 `apiContractDigest`，同一份契约必然算出同一个值——把服务端响应头里的 `x-dyyto-contract-digest` 与本地 spec 的 `digest` 对上，就知道手上的客户端是不是过期的。\n\n不带 `--expect` 时 `digest` 只把摘要打到 stdout，便于流水线捕获后再比对。\n\n## 错误与边界\n\n- `API_ROUTE_DEFINITION_INVALID`：定义非法（operationId 格式、路径穿越、GET 携带 body、路径参数与 `params` schema 不匹配、错误码非大写常量、`successStatus` 用在 GET/DELETE 上）。**装配期即失败。**\n- `API_ROUTE_HANDLER_MISSING`：路由没有对应处理器。\n- `API_ROUTE_RESPONSE_INVALID`：处理器产出违约载荷。**畸形载荷绝不发出**，返回信封 500，缺陷通过 `onDefect` Port 交给组合根。\n\n其他边界：\n\n- 请求输入在处理器运行前校验；非法输入不会到达业务代码。\n- 失败也是契约的一部分：声明过的错误码会进入 OpenAPI 文档的 4xx/5xx 响应。\n- 契约摘要挂在响应头，不进响应体——它是跨切面事实，不属于任何一条路由。\n- 一个服务由多个路由器组成时，`publishContractDigest` 传函数让它们公布**同一个**合并契约的摘要。\n- 本包不写日志，不记录请求体或响应体。\n\n## 退出方式\n\n路由定义是纯数据，schema 由调用方拥有。替换 `createApiRouter` 后删除依赖即可；已导出的 OpenAPI 文档与生成产物仍可继续使用。\n","readmeFilename":""}