This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/packages/contracts/src/api/http.ts
T
xcsandClaude Fable 5 8556432dba feat(org): 成员目录 —— Keycloak 读写面穿透、治理台成员/组织/权限三页、姓在前 personName、Staff.kind
后端:`KeycloakDirectory` 端口(users / groups / roles 读写;写直接穿透 Keycloak —— 这里建的人立刻能登录、
删的人立刻消失,停用与收权之后 logout 踢掉会话)、`keycloak-directory.ts` + `keycloak-admin-http.ts` 适配器、
org-directory 契约与 /api/gov 路由、`sync-roster` CLI(ops 路由够不着时整册照过来),
容器按 provisioning 服务账号有无决定注不注入(没配就如实说不可用)。
`personName` 姓在前:汉字直拼「柴浩」、否则留一个空格「Admin Amy」,token 与名册同步两条路同一把尺;
`staffKindOf` 按 Keycloak 固定前缀 service-account- 认服务账号,员工那一屏不摆它(`Staff.kind` 不落库,按账号名现判)。
前端:员工那一屏重做成成员 / 组织 / 权限三页签(member-page 下钻),页签栏右端一句话说清这一格管什么。
契约变更:contracts org.ts / api/http.ts / api/org-directory.ts;api-report 已刷新;org CONTRACT.md 与 ADR-012 同步。
2026-08-26 做的,一直留在工作树没入库。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0167qWjeyBwvfdGovd7FWbCu
2026-08-27 11:44:36 +08:00

82 lines
2.8 KiB
TypeScript

/**
* 路由契约的形状(编码规范 §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<typeof surfaceSchema>;
export const SURFACE_LABEL: Readonly<Record<Surface, string>> = {
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<R extends RouteContract> = z.infer<R['output']>;
export type RouteBody<R extends RouteContract> = R['body'] extends z.ZodTypeAny
? z.infer<R['body']>
: 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;
};