org domain:claims 归一、角色→端与锁死不变量、名册差异、测试账号策略(ADR-012,纯函数)

为统一鉴权中心(Keycloak)落地铺地基:五个零 IO 的纯函数文件,后续 org 用例
重写(Task 8)直接复用。本任务只加不删 —— 旧的岗位授权 / 口令会话 / 停用检查
原样保留,现役 org-service 仍在用它们。

- claims.ts:token claims → Identity,滤掉三种 Keycloak 家具角色
- ends.ts:角色 → 端的并集,并守住「改完之后至少一人能进运营中心」这条锁死不变量
- roster-diff.ts:与 Keycloak 名册的插/改/停/复活差异(远端说了算)
- test-account.ts:测试账号判定与独立工作组构造(ADR-011 语义原样搬)
- in-flight.ts:停用前置检查要看的在途工作(与旧 disable-check.ts 并存一轮)

contract.ts / port.ts / index.ts / seed-data.ts 均为文件末尾追加,契约变更已
经 pnpm api:check --update 刷新快照。
This commit is contained in:
xcs
2026-08-25 16:09:10 +08:00
parent a5c8d4786d
commit 5108d7a580
16 changed files with 515 additions and 0 deletions
+38
View File
@@ -93,3 +93,41 @@ export type OrgService = {
staffOfPosition(positionCode: string): Promise<StaffWithPosition[]>;
ensureSeeded(): Promise<void>;
};
/* ── 统一鉴权中心(ADR-012) ──────────────────────────────────────────── */
/** 从 token 或 Admin API 归一出来的一个人 —— `sub` 即 Staff.id */
export type Identity = {
readonly sub: string;
/** preferred_username */
readonly account: string;
/** name claim,缺则回退 account */
readonly name: string;
readonly email?: string;
/** realm 角色,噪声角色已滤、已排序 */
readonly roles: readonly string[];
};
/** 验签没过 —— message 是手册 4.4 的人话,中间件原样回 401 */
export class TokenRejectedError extends Error {
constructor(message: string) {
super(message);
this.name = 'TokenRejectedError';
}
}
/** 名册同步标了停用 —— 即使 token 还有效也拒(403);Keycloak 恢复后再同步一次 */
export class AccountDisabledError extends Error {
constructor(readonly account: string) {
super(`账号已在鉴权中心停用:${account} —— 若已恢复,请在运营中心同步一次名册`);
this.name = 'AccountDisabledError';
}
}
/** 角色→端改完之后没有任何在册员工能进运营中心 —— 那张表就再也没人改得了 */
export class AccessLockoutError extends Error {
constructor() {
super('改完之后没有任何在册员工能进运营中心 —— 这张表就再也改不回来了,拒绝');
this.name = 'AccessLockoutError';
}
}