/** * 路由契约的形状(编码规范 §8.1 / §9.2)。 * * 前端只经 `shared/typed-fetch` + 本目录的路由契约与服务端交互;组件内禁止裸 fetch。 * **禁止 Hono RPC 客户端** —— 它要求前端 import 后端类型,违反 §5.1。 */ import { z } from 'zod'; export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const; export type HttpMethod = (typeof HTTP_METHODS)[number]; /** * 三个端(ADR-012):设计中心 / 运营中心 / 工作台。 * 权限只到端 —— 一个角色能进哪几个端由运营中心的「角色 → 端」表定,不写死在代码里。 */ export const SURFACES = ['design', 'ops', 'workbench'] as const; export const surfaceSchema = z.enum(SURFACES); export type Surface = z.infer; export const SURFACE_LABEL: Readonly> = { design: '设计中心', ops: '运营中心', workbench: '工作台', }; /** * 一条路由归哪个端(ADR-012): * `public` 不需要凭据;`any` 登录即可、不看端;端名 / 端名数组 —— 能进其中任一端即可。 * 鉴权中间件按它统一判,路由里不再各写守卫;漏写编译不过(`satisfies RouteContract`)。 */ export type RouteAccess = 'public' | 'any' | Surface | readonly Surface[]; /** * 一条路由契约。`path` 用 `:name` 表示路径参数,与 `params` 的键一一对应。 * body 只在 POST/PUT/PATCH 上有意义;GET 用 query。 */ export type RouteContract< TParams extends z.ZodTypeAny = z.ZodTypeAny, TQuery extends z.ZodTypeAny = z.ZodTypeAny, TBody extends z.ZodTypeAny = z.ZodTypeAny, TOutput extends z.ZodTypeAny = z.ZodTypeAny, > = { readonly method: HttpMethod; readonly path: string; readonly access: RouteAccess; readonly params?: TParams; readonly query?: TQuery; readonly body?: TBody; readonly output: TOutput; }; export type RouteOutput = z.infer; export type RouteBody = R['body'] extends z.ZodTypeAny ? z.infer : never; /** 结构化错误:前端按 code 分辨,不解析文案。`gone` = 410,路由已随统一鉴权中心退役 */ export const API_ERROR_CODES = [ 'unauthenticated', 'forbidden', 'not-found', 'validation-failed', 'conflict', 'precondition-failed', 'internal', 'gone', /* * 成员目录专用的两档 502(ADR-012)—— **重试有没有意义**是它们的分界, * 前端据此说两句不同的话:一句「找管理员补权限」,一句「过会儿再点」。 * 混成一个 code 的话,界面只能说一句含糊的「出错了」。 */ 'keycloak-misconfigured', 'keycloak-unavailable', ] as const; export type ApiErrorCode = (typeof API_ERROR_CODES)[number]; export type ApiError = { readonly code: ApiErrorCode; readonly message: string; readonly detail?: unknown; };