Archived
契约:三个端与路由归属(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
This commit is contained in:
@@ -4,39 +4,40 @@
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { sessionSchema } from '../org.ts';
|
||||
import { principalSchema, sessionSchema } from '../org.ts';
|
||||
import { chatMessageSchema, deliverableSchema, fileDocSchema, formFieldSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
|
||||
import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||
import { staffIdSchema, taskIdSchema } from '../id.ts';
|
||||
|
||||
/** 我是谁 —— 无会话即 401,前端据此落登录页(ADR-010) */
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const sessionRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/session',
|
||||
access: 'public',
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 登录 —— 单组织 cookie session(编码规范 §4 / ADR-010)。
|
||||
* 成功即种 `jcai_session`(HttpOnly);失败只回一句话,不区分账号不存在还是口令错。
|
||||
*/
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const loginRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/login',
|
||||
access: 'public',
|
||||
body: z.object({ account: z.string().min(1), password: z.string().min(1) }),
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 登出 —— 服务端会话立刻作废,不等 cookie 到期 */
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const logoutRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/logout',
|
||||
access: 'public',
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const healthRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/health',
|
||||
access: 'public',
|
||||
output: z.object({
|
||||
ok: z.boolean(),
|
||||
version: z.string(),
|
||||
@@ -44,6 +45,25 @@ export const healthRoute = {
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 我是谁、我能进哪几个端(ADR-012)—— 中间件解好 principal 放进上下文,这里只回投影 */
|
||||
export const meRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/me',
|
||||
access: 'any',
|
||||
output: principalSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 60 秒票据(ADR-012 §9.3)—— 只为两条带不了 Authorization 头的路:
|
||||
* `EventSource('/api/stream')` 与 `<a href="…&raw=1" download>`。GET 上 `?ticket=` 认它。
|
||||
*/
|
||||
export const authTicketRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/auth/ticket',
|
||||
access: 'any',
|
||||
output: z.object({ ticket: z.string().min(1), expiresAt: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 任务要素:四区(任务列表 / 标题区 / 工作区 / 对话区) ─────────────── */
|
||||
|
||||
/** 任务卡 —— 清单里一条 = 一个活动的一次运行,不是一整条工作流 */
|
||||
@@ -77,6 +97,7 @@ export type TaskCard = z.infer<typeof taskCardSchema>;
|
||||
export const listTasksRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks',
|
||||
access: 'workbench',
|
||||
output: z.object({ tasks: z.array(taskCardSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -229,6 +250,7 @@ export type TaskDetail = z.infer<typeof taskDetailSchema>;
|
||||
export const taskDetailRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks/:taskId',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: taskDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
@@ -237,6 +259,7 @@ export const taskDetailRoute = {
|
||||
export const replyWorkOrderRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/work-orders/reply',
|
||||
access: 'workbench',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean(), producesCompletion: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -248,6 +271,7 @@ export const replyWorkOrderRoute = {
|
||||
export const replyChatCardRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/chat-cards/reply',
|
||||
access: 'workbench',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -259,6 +283,7 @@ export const replyChatCardRoute = {
|
||||
export const markCommReadRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/comms/:commId/read',
|
||||
access: 'workbench',
|
||||
params: z.object({ commId: z.string() }),
|
||||
body: z.object({ read: z.boolean() }),
|
||||
output: z.object({ ok: z.boolean(), unread: z.boolean() }),
|
||||
@@ -301,6 +326,7 @@ export const markCommReadRoute = {
|
||||
export const saveTableRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/table',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({
|
||||
/** `{ '域.对象': [ { 台账列名: 值 } ] }` —— 主键列用来认行,不参与改行;台账里没有的行就是**新增** */
|
||||
@@ -347,6 +373,7 @@ export const saveTableRoute = {
|
||||
export const openAssetFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks/:taskId/file',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
query: z.object({
|
||||
/** `域.对象` —— 必须是活动清单里声明过 `file` 段的那张表(授权面) */
|
||||
@@ -362,6 +389,7 @@ export const openAssetFileRoute = {
|
||||
export const sayRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/say',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({ text: z.string().min(1) }),
|
||||
output: chatMessageSchema,
|
||||
@@ -402,12 +430,14 @@ export type SystemMessageView = z.infer<typeof systemMessageSchema>;
|
||||
export const listMessagesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/messages',
|
||||
access: 'workbench',
|
||||
output: z.object({ messages: z.array(systemMessageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const markMessageReadRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/messages/:messageId/read',
|
||||
access: 'workbench',
|
||||
params: z.object({ messageId: z.string() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -416,6 +446,7 @@ export const markMessageReadRoute = {
|
||||
export const answerMessageRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/messages/:messageId/answer',
|
||||
access: 'workbench',
|
||||
params: z.object({ messageId: z.string() }),
|
||||
body: z.object({ answer: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
@@ -425,6 +456,7 @@ export const answerMessageRoute = {
|
||||
export const confirmTriggerPendingRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/trigger-pending/:id/confirm',
|
||||
access: 'workbench',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ workflowId: z.string(), alreadyStarted: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -460,12 +492,14 @@ export type MinuteView = z.infer<typeof minuteSchema>;
|
||||
export const listMinutesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/minutes',
|
||||
access: 'workbench',
|
||||
output: z.object({ minutes: z.array(minuteSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const createMinuteRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes',
|
||||
access: 'workbench',
|
||||
body: z.object({
|
||||
title: z.string().min(1),
|
||||
body: z.string(),
|
||||
@@ -479,6 +513,7 @@ export const createMinuteRoute = {
|
||||
export const appendResolutionRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes/:minuteId/resolutions',
|
||||
access: 'workbench',
|
||||
params: z.object({ minuteId: z.string() }),
|
||||
body: z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() }),
|
||||
output: minuteSchema,
|
||||
@@ -488,6 +523,7 @@ export const appendResolutionRoute = {
|
||||
export const registerResolutionRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes/resolutions/:resolutionId/register',
|
||||
access: 'workbench',
|
||||
params: z.object({ resolutionId: z.string() }),
|
||||
body: z.object({ processPackageId: z.string().min(1), data: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ workflowId: z.string(), resolutionId: z.string() }),
|
||||
@@ -542,6 +578,7 @@ export type ReportView = z.infer<typeof reportSchema>;
|
||||
export const draftReportRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/reports/:period',
|
||||
access: 'workbench',
|
||||
params: z.object({ period: reportPeriodSchema }),
|
||||
output: reportSchema,
|
||||
} as const satisfies RouteContract;
|
||||
@@ -549,6 +586,7 @@ export const draftReportRoute = {
|
||||
export const listReportsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/reports',
|
||||
access: 'workbench',
|
||||
output: z.object({ reports: z.array(reportSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -556,6 +594,7 @@ export const listReportsRoute = {
|
||||
export const submitReportRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/reports/:reportId/submit',
|
||||
access: 'workbench',
|
||||
params: z.object({ reportId: z.string() }),
|
||||
body: z.object({ note: z.string().optional() }),
|
||||
output: reportSchema,
|
||||
@@ -585,6 +624,7 @@ export type ApprovalItem = z.infer<typeof approvalItemSchema>;
|
||||
export const listApprovalsForMeRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/approvals',
|
||||
access: 'workbench',
|
||||
output: z.object({ approvals: z.array(approvalItemSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -592,6 +632,7 @@ export const listApprovalsForMeRoute = {
|
||||
export const decideApprovalRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/approvals/:workflowId/:nodeId/decide',
|
||||
access: 'workbench',
|
||||
params: z.object({ workflowId: z.string(), nodeId: z.string() }),
|
||||
body: z.object({ verdict: z.enum(['pass', 'reject']), comment: z.string().optional() }),
|
||||
output: z.object({ ok: z.boolean(), workflowState: workflowStateSchema }),
|
||||
@@ -608,6 +649,7 @@ export type ListApprovalsForMeOutput = z.infer<typeof listApprovalsForMeRoute.ou
|
||||
export const listRegistrationTriggersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/triggers/registration',
|
||||
access: 'workbench',
|
||||
output: z.object({
|
||||
triggers: z.array(
|
||||
z.object({
|
||||
@@ -625,6 +667,7 @@ export const listRegistrationTriggersRoute = {
|
||||
export const fireRegistrationRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/triggers/:triggerId/register',
|
||||
access: 'workbench',
|
||||
params: z.object({ triggerId: z.string() }),
|
||||
body: z.object({ data: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({
|
||||
|
||||
Reference in New Issue
Block a user