Archived
- packages/contracts/src/api/http.ts:新增 SURFACES / surfaceSchema / Surface / SURFACE_LABEL / RouteAccess;RouteContract.access 必填;API_ERROR_CODES 增 'gone'。 - packages/contracts/src/org.ts:staffSchema 补 email? / roles(realm 角色快照);新增 principalSchema/Principal、roleAccessSchema/RoleAccess、rosterSyncOutcomeSchema/RosterSyncOutcome、 inFlightWorkSchema/InFlightWork;旧岗位授权(grant* / governanceFunction*)与 sessionSchema/Session 全部标 @deprecated 留一轮(web 编译不受影响,下个迭代删)。 - workbench.ts / governance.ts / ingest.ts:每条既有路由补 access(按所属端/两端共用/退役 public); 新增 meRoute、authTicketRoute(access:'any')与 syncStaffRoute、setStaffPositionRoute、 staffInFlightRoute、listRoleAccessRoute、setRoleAccessRoute、deleteRoleAccessRoute(access:'ops')。 - apps/api 的 org-service.ts 两处 staff.insert 补 roles: [](staffSchema 新必填字段,该文件下个 迭代会整份替换,这里只是让本次 typecheck 过)。 - 新增 packages/contracts/src/__tests__/route-access.test.ts:95 条路由的 access 归属断言。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
/** 路由契约:每条都得说清归哪个端(ADR-012)—— 中间件靠它,漏一条就是一个没人管的口子 */
|
|
import { describe, expect, it } from 'vitest';
|
|
import * as contracts from '../index.ts';
|
|
import type { RouteContract } from '../index.ts';
|
|
import { SURFACES } from '../index.ts';
|
|
|
|
function isRoute(value: unknown): value is RouteContract {
|
|
if (typeof value !== 'object' || value === null) return false;
|
|
const v = value as { method?: unknown; path?: unknown; output?: unknown };
|
|
return typeof v.method === 'string' && typeof v.path === 'string' && v.output !== undefined;
|
|
}
|
|
|
|
/**
|
|
* `contracts` 是异质的命名空间(schema / 常量 / 路由混杂),`Object.entries` 推出的元素类型
|
|
* 因而是一个几百项的大联合;`entry is [string, RouteContract]` 直接对着那个联合做型别谓词,
|
|
* TS 判不出谓词类型与参数类型互相兼容(TS2677)。先收窄成 `[string, unknown][]` 再筛 ——
|
|
* 语义不变(照旧靠 `isRoute` 的运行时判断筛路由),只是把关系判断让给 `unknown`。
|
|
*/
|
|
const routes = (Object.entries(contracts) as [string, unknown][]).filter(
|
|
(entry): entry is [string, RouteContract] => isRoute(entry[1]),
|
|
);
|
|
const surfaces: readonly string[] = SURFACES;
|
|
|
|
describe('路由契约的归属(access)', () => {
|
|
it('收得到全部路由(至少 90 条)', () => {
|
|
expect(routes.length).toBeGreaterThanOrEqual(90);
|
|
});
|
|
|
|
it('access 只取 public / any / 端名 / 非空端名数组', () => {
|
|
for (const [name, route] of routes) {
|
|
const a = route.access;
|
|
const ok =
|
|
a === 'public' ||
|
|
a === 'any' ||
|
|
(typeof a === 'string' ? surfaces.includes(a) : a.length > 0 && a.every((s) => surfaces.includes(s)));
|
|
expect(ok, `${name}.access = ${JSON.stringify(a)}`).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('公开路由只有健康检查、入站邮件与退役八条', () => {
|
|
const publicNames = routes.filter(([, r]) => r.access === 'public').map(([n]) => n).sort();
|
|
expect(publicNames).toEqual(
|
|
[
|
|
'healthRoute',
|
|
'ingestMailRoute',
|
|
'loginRoute',
|
|
'logoutRoute',
|
|
'sessionRoute',
|
|
'createStaffRoute',
|
|
'disableStaffCheckRoute',
|
|
'disableStaffRoute',
|
|
'listGrantsRoute',
|
|
'setGrantsRoute',
|
|
].sort(),
|
|
);
|
|
});
|
|
|
|
it('只要登录不看端的只有 me / 票据', () => {
|
|
const anyNames = routes.filter(([, r]) => r.access === 'any').map(([n]) => n).sort();
|
|
expect(anyNames).toEqual(['authTicketRoute', 'meRoute']);
|
|
});
|
|
|
|
it('两端共用的读路由就是 spec §4.4 点名的那八条', () => {
|
|
const both = routes
|
|
.filter(([, r]) => Array.isArray(r.access))
|
|
.map(([n]) => n)
|
|
.sort();
|
|
expect(both).toEqual(
|
|
[
|
|
'listStaffRoute',
|
|
'listProcessPackagesRoute',
|
|
'listTriggersRoute',
|
|
'listChannelsRoute',
|
|
'listLedgerObjectsRoute',
|
|
'ledgerByActivityRoute',
|
|
'browseLedgerRoute',
|
|
'govWorkflowRoute',
|
|
].sort(),
|
|
);
|
|
});
|
|
});
|