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/org.ts
T
xcsandClaude Opus 5 a5c8d4786d 契约:三个端与路由归属(access 必填)、Principal / 角色→端 / 名册同步 schema、退役路由标 @deprecated(ADR-012)
- 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
2026-08-25 15:54:20 +08:00

186 lines
6.9 KiB
TypeScript

/**
* 员工、岗位与端(编码规范 §4 · `org`;语义出处 OS 02 员工管理;鉴权归统一鉴权中心,ADR-012)。
*
* 身份来自 Keycloak:`Staff.id` 就是 token 的 `sub`。权限只到「端」(design / ops / workbench),
* 角色 → 端由运营中心的表定;岗位仍是本系统的表(流程引擎按它解析负责人)。
* 旧的岗位授权(客体三类 × 动作三种)与口令会话已退役 —— 类型留一轮(@deprecated,下个迭代删)。
*/
import { z } from 'zod';
import { positionIdSchema, staffIdSchema } from './id.ts';
import { surfaceSchema } from './api/http.ts';
export const positionSchema = z.object({
id: positionIdSchema,
code: z.string().min(1),
name: z.string().min(1),
note: z.string().optional(),
});
export type Position = z.infer<typeof positionSchema>;
export const STAFF_STATES = ['active', 'disabled'] as const;
export const staffStateSchema = z.enum(STAFF_STATES);
export type StaffState = z.infer<typeof staffStateSchema>;
export const staffSchema = z.object({
/** = Keycloak `sub`(手册硬约束 8:业务外键用 sub,不用用户名) */
id: staffIdSchema,
/** = `preferred_username`,只作显示 —— 用户名允许改,别拿它当键 */
account: z.string().min(1),
/** = `name` claim,缺则回退 account */
name: z.string().min(1),
email: z.string().optional(),
/** 未指派 = 哨兵岗 `NONE / 未分配` 的 id */
positionId: positionIdSchema,
/** 只由名册同步置 `disabled`(Keycloak 停用或删除);有效 token 不改它 */
state: staffStateSchema.default('active'),
/** realm 角色快照(token 签发时;噪声角色已滤) */
roles: z.array(z.string()).default([]),
});
export type Staff = z.infer<typeof staffSchema>;
/* ── 我是谁、能进哪几个端(ADR-012) ───────────────────────────────── */
export const principalSchema = z.object({
staff: staffSchema,
position: positionSchema,
roles: z.array(z.string()),
ends: z.array(surfaceSchema),
});
export type Principal = z.infer<typeof principalSchema>;
/** 角色 → 端:一行一个 realm 角色 */
export const roleAccessSchema = z.object({
role: z.string().min(1),
ends: z.array(surfaceSchema),
});
export type RoleAccess = z.infer<typeof roleAccessSchema>;
/** 名册同步的结果 —— 没配 provisioning 就如实说没配,不摆零 */
export const rosterSyncOutcomeSchema = z.discriminatedUnion('configured', [
z.object({ configured: z.literal(false), message: z.string() }),
z.object({
configured: z.literal(true),
inserted: z.number().int().min(0),
updated: z.number().int().min(0),
disabled: z.number().int().min(0),
total: z.number().int().min(0),
}),
]);
export type RosterSyncOutcome = z.infer<typeof rosterSyncOutcomeSchema>;
/* ── 在途工作(只读;OS 02:改派先于停用,停用本身已归鉴权中心) ───── */
export const inFlightWorkSchema = z.object({
staffId: staffIdSchema,
blocked: z.boolean(),
openTasks: z.array(z.object({ taskId: z.string(), title: z.string() })).default([]),
openCards: z.array(z.object({ cardId: z.string(), title: z.string() })).default([]),
openApprovals: z.array(z.object({ approvalId: z.string(), title: z.string() })).default([]),
});
export type InFlightWork = z.infer<typeof inFlightWorkSchema>;
/** @deprecated 随 ADR-012 改名 `inFlightWorkSchema`;下个迭代删 */
export const disableCheckSchema = inFlightWorkSchema;
/** @deprecated 改用 `InFlightWork` */
export type DisableCheck = InFlightWork;
/* ── 岗位授权(已退役,ADR-012;类型留一轮给 web 编译,下个迭代删) ──── */
/** @deprecated 权限只到端,动作三分已退役 */
export const GRANT_ACTIONS = ['read', 'write', 'govern'] as const;
/** @deprecated */
export const grantActionSchema = z.enum(GRANT_ACTIONS);
/** @deprecated */
export type GrantAction = z.infer<typeof grantActionSchema>;
/** @deprecated */
export const GRANT_ACTION_LABEL: Readonly<Record<GrantAction, string>> = {
read: '读',
write: '写',
govern: '治理',
};
/** @deprecated 治理台九功能不再是授权客体 */
export const GOVERNANCE_FUNCTIONS = [
'task',
'staff',
'process',
'trigger',
'activity',
'tool',
'data',
'memory',
'system',
] as const;
/** @deprecated */
export const governanceFunctionSchema = z.enum(GOVERNANCE_FUNCTIONS);
/** @deprecated */
export type GovernanceFunction = z.infer<typeof governanceFunctionSchema>;
/** @deprecated */
export const GOVERNANCE_FUNCTION_LABEL: Readonly<Record<GovernanceFunction, string>> = {
task: '任务管理',
staff: '员工管理',
process: '工作流管理',
trigger: '触发器配置',
activity: '活动库管理',
tool: '工具管理',
data: '数据管理',
memory: '记忆管理',
system: '系统管理',
};
/** @deprecated */
export const grantObjectSchema = z.discriminatedUnion('kind', [
z.object({
kind: z.literal('data'),
domain: z.string().min(1),
object: z.string().min(1),
field: z.string().optional(),
}),
z.object({ kind: z.literal('governance'), function: governanceFunctionSchema }),
z.object({ kind: z.literal('task'), activityId: z.string().optional() }),
]);
/** @deprecated */
export type GrantObject = z.infer<typeof grantObjectSchema>;
/** @deprecated */
export const grantSchema = z.object({
positionId: positionIdSchema,
object: grantObjectSchema,
action: grantActionSchema,
});
/** @deprecated */
export type Grant = z.infer<typeof grantSchema>;
/* ── 会话(已退役:身份在 token 里,ADR-012) ──────────────────────── */
/** @deprecated 改用 `principalSchema` / `GET /api/me` */
export const sessionSchema = z.object({
staff: staffSchema,
position: positionSchema,
grants: z.array(grantSchema),
});
/** @deprecated 改用 `Principal` */
export type Session = z.infer<typeof sessionSchema>;
/* ── 工作组(ADR-011) ────────────────────────────────────────────────── */
/**
* 工作组 —— 一组「岗位 → 员工」的绑定,起单时装载、随工作流走。
*
* **不是第五种负责人策略**(OS 03「策略封闭四选」不动):
* 四策略回答的是「这个岗位该派给谁」,工作组直接给出了答案,策略于是无从发问。
* 键是**岗位 code**(BD / PM / FAE …),与流程包 `owner.position` 同一口径。
*/
export const workGroupSchema = z.object({
id: z.string().min(1),
name: z.string().min(1),
/** 组归谁 —— 起单时按来源人取其组 */
ownerId: staffIdSchema,
members: z.record(z.string().min(1), staffIdSchema),
});
export type WorkGroup = z.infer<typeof workGroupSchema>;
/** 装进工作流的**快照**:改组不改已起的单(留痕不被改写) */
export const workGroupBindingSchema = z.record(z.string().min(1), staffIdSchema);
export type WorkGroupBinding = z.infer<typeof workGroupBindingSchema>;