/** * module: org —— 对外类型(编码规范 §10.1)。只有类型和常量,没有实现。 * * 身份归统一鉴权中心(ADR-012):`Staff.id` 就是 Keycloak 的 `sub`。 * 本模块管三件事:①验签后的人是谁(名册即时更新);②他能进哪几个端(角色→端);③岗位与工作组。 * 口令 / 会话 / 岗位授权(客体三类 × 动作三种)已退役。 */ import type { InFlightWork, Position, PositionId, Principal, RoleAccess, RosterSyncOutcome, Staff, StaffId, Surface, WorkGroup, WorkGroupBinding, } from '@contracts'; export type { InFlightWork, Position, Principal, RoleAccess, RosterSyncOutcome, Staff, Surface, WorkGroup, WorkGroupBinding }; /** 员工连同其岗位的读模型 —— 治理台员工管理列表用 */ export type StaffWithPosition = Staff & { readonly position: Position }; /** 从 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[]; }; export class StaffNotFoundError extends Error { constructor(readonly staffId: StaffId) { super(`员工不存在:${staffId}`); this.name = 'StaffNotFoundError'; } } export class PositionNotFoundError extends Error { constructor(readonly positionCode: string) { super(`岗位不存在:${positionCode}`); this.name = 'PositionNotFoundError'; } } /** 验签没过 —— 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'; } } /** * 这一步做下去就没有任何在册活跃员工能进运营中心了 —— 拒绝(路由映射 409)。 * 两个来路:改「角色 → 端」(那张表就再也没人改得了)、同步名册(同步按钮自己也要 ops)。 * `message` 可覆盖:拒绝的理由不同,给人的下一步也不同。 */ export class AccessLockoutError extends Error { constructor(message = '改完之后没有任何在册员工能进运营中心 —— 这张表就再也改不回来了,拒绝') { super(message); this.name = 'AccessLockoutError'; } } export type OrgService = { /* ── 身份(ADR-012)—— 中间件在 app 层,身份判定读这里 ─────────────── */ /** 验签 → 名册即时更新 → 角色→端。抛 TokenRejectedError(401)/ AccountDisabledError(403) */ authenticate(token: string): Promise; /** 票据那条路:按 sub 从库现算;停用抛 AccountDisabledError;不存在回 undefined */ principalOf(staffId: StaffId): Promise; /** 名册即时更新的入口 —— 没有就建(NONE 岗、active),有就把变了的列写回;测试也用它造人 */ registerIdentity(identity: Identity): Promise; /* ── 名册与岗位 ───────────────────────────────────────────────────── */ listStaff(): Promise<{ staff: StaffWithPosition[]; positions: Position[] }>; getStaff(staffId: StaffId): Promise; /** 岗位在岗人员 —— 负责人四策略解析的候选集来源(由 process 调用);只回 active;NONE 恒回 [] */ staffOfPosition(positionCode: string): Promise; setPosition(staffId: StaffId, positionId: PositionId): Promise; /** Admin API 同步;未配 provisioning 回 configured:false */ syncRoster(): Promise; /** 此人身上还挂着什么(只读;OS 02 改派先于停用) */ inFlightOf(staffId: StaffId): Promise; /* ── 角色 → 端 ───────────────────────────────────────────────────── */ listRoleAccess(): Promise<{ roles: RoleAccess[]; knownRoles: string[] }>; /** 抛 AccessLockoutError(409):改完没有任何在册活跃员工能进运营中心 */ setRoleAccess(role: string, ends: readonly Surface[]): Promise; deleteRoleAccess(role: string): Promise; /** 能进该端的活跃员工 —— 治理台通知的收件人 */ staffWithEnd(surface: Surface): Promise; /* ── 工作组(ADR-011)—— 起单时的岗位绑定,不是第五种策略 ────────────── */ /** 建组 / 改组;id 留空即新建 */ saveWorkGroup(input: { id?: string; name: string; ownerId: StaffId; members: Readonly>; }): Promise; /** 某人的工作组 —— 起单时按来源人取,取不到即不装载(全走四策略) */ workGroupOf(staffId: StaffId): Promise; /** 装进工作流的快照:改组不改已起的单 */ bindingOf(staffId: StaffId): Promise; /** 岗位(含 NONE / TEST)+ 缺省角色→端(表空时)+ 已在册测试账号的策略;幂等 */ ensureSeeded(): Promise; };