baa-conductor


baa-conductor / packages / auth / src
im_wower  ·  2026-03-22

control-api.ts

  1import type { AuthAction } from "./actions.js";
  2import type { AuthPrincipal, AuthResourceOwnership } from "./model.js";
  3import { authorize, type AuthDecision } from "./policy.js";
  4
  5export type ControlApiMethod = "GET" | "POST";
  6
  7export interface ControlApiAuthRule {
  8  method: ControlApiMethod;
  9  pathPattern: string;
 10  action: AuthAction;
 11  summary: string;
 12}
 13
 14export interface ControlApiAuthorizationInput {
 15  method: ControlApiMethod;
 16  path: string;
 17  principal: AuthPrincipal;
 18  resource?: AuthResourceOwnership;
 19}
 20
 21export interface ControlApiAuthorizationResult extends AuthDecision {
 22  matchedRule?: ControlApiAuthRule;
 23}
 24
 25export const CONTROL_API_AUTH_RULES = [
 26  {
 27    method: "POST",
 28    pathPattern: "/v1/controllers/heartbeat",
 29    action: "controllers.heartbeat",
 30    summary: "controller 心跳"
 31  },
 32  {
 33    method: "POST",
 34    pathPattern: "/v1/leader/acquire",
 35    action: "leader.acquire",
 36    summary: "获取或续租 leader lease"
 37  },
 38  {
 39    method: "GET",
 40    pathPattern: "/v1/controllers",
 41    action: "controllers.list",
 42    summary: "列出 controller 摘要"
 43  },
 44  {
 45    method: "POST",
 46    pathPattern: "/v1/tasks",
 47    action: "tasks.create",
 48    summary: "创建 task"
 49  },
 50  {
 51    method: "GET",
 52    pathPattern: "/v1/tasks",
 53    action: "tasks.list",
 54    summary: "列出 task 摘要"
 55  },
 56  {
 57    method: "POST",
 58    pathPattern: "/v1/tasks/claim",
 59    action: "tasks.claim",
 60    summary: "领取待规划 task 或 runnable step"
 61  },
 62  {
 63    method: "POST",
 64    pathPattern: "/v1/tasks/:task_id/plan",
 65    action: "tasks.plan",
 66    summary: "持久化已验收 plan"
 67  },
 68  {
 69    method: "POST",
 70    pathPattern: "/v1/steps/:step_id/heartbeat",
 71    action: "steps.heartbeat",
 72    summary: "step 心跳"
 73  },
 74  {
 75    method: "POST",
 76    pathPattern: "/v1/steps/:step_id/checkpoint",
 77    action: "steps.checkpoint",
 78    summary: "写 step checkpoint"
 79  },
 80  {
 81    method: "POST",
 82    pathPattern: "/v1/steps/:step_id/complete",
 83    action: "steps.complete",
 84    summary: "标记 step 完成"
 85  },
 86  {
 87    method: "POST",
 88    pathPattern: "/v1/steps/:step_id/fail",
 89    action: "steps.fail",
 90    summary: "标记 step 失败"
 91  },
 92  {
 93    method: "POST",
 94    pathPattern: "/v1/system/pause",
 95    action: "system.pause",
 96    summary: "暂停自动化"
 97  },
 98  {
 99    method: "POST",
100    pathPattern: "/v1/system/resume",
101    action: "system.resume",
102    summary: "恢复自动化"
103  },
104  {
105    method: "POST",
106    pathPattern: "/v1/system/drain",
107    action: "system.drain",
108    summary: "drain 自动化"
109  },
110  {
111    method: "POST",
112    pathPattern: "/v1/system/promote",
113    action: "maintenance.promote",
114    summary: "promote 维护操作"
115  },
116  {
117    method: "POST",
118    pathPattern: "/v1/system/demote",
119    action: "maintenance.demote",
120    summary: "demote 维护操作"
121  },
122  {
123    method: "GET",
124    pathPattern: "/v1/system/state",
125    action: "system.state.read",
126    summary: "读取系统状态"
127  },
128  {
129    method: "GET",
130    pathPattern: "/v1/runs",
131    action: "runs.list",
132    summary: "列出 run 摘要"
133  },
134  {
135    method: "GET",
136    pathPattern: "/v1/tasks/:task_id/logs",
137    action: "tasks.logs.read",
138    summary: "读取 task 日志"
139  },
140  {
141    method: "GET",
142    pathPattern: "/v1/tasks/:task_id",
143    action: "tasks.read",
144    summary: "读取 task 详情"
145  },
146  {
147    method: "GET",
148    pathPattern: "/v1/runs/:run_id",
149    action: "runs.read",
150    summary: "读取 run 详情"
151  }
152] satisfies ReadonlyArray<ControlApiAuthRule>;
153
154export function findControlApiAuthRule(
155  method: ControlApiMethod,
156  path: string
157): ControlApiAuthRule | null {
158  for (const rule of CONTROL_API_AUTH_RULES) {
159    if (rule.method === method && matchesRoutePattern(rule.pathPattern, path)) {
160      return rule;
161    }
162  }
163
164  return null;
165}
166
167export function authorizeControlApiRoute(
168  input: ControlApiAuthorizationInput
169): ControlApiAuthorizationResult {
170  const matchedRule = findControlApiAuthRule(input.method, input.path);
171
172  if (!matchedRule) {
173    return {
174      ok: false,
175      reason: "route_not_modeled",
176      statusCode: 404
177    };
178  }
179
180  const decision = authorize({
181    principal: input.principal,
182    action: matchedRule.action,
183    resource: input.resource
184  });
185
186  return {
187    ...decision,
188    matchedRule
189  };
190}
191
192export function describeControlApiAuthSurface(): string[] {
193  return CONTROL_API_AUTH_RULES.map(
194    (rule) => `${rule.method} ${rule.pathPattern} -> ${rule.action} (${rule.summary})`
195  );
196}
197
198function matchesRoutePattern(pattern: string, actualPath: string): boolean {
199  const patternSegments = normalizePath(pattern);
200  const actualSegments = normalizePath(actualPath);
201
202  if (patternSegments.length !== actualSegments.length) {
203    return false;
204  }
205
206  return patternSegments.every((segment, index) => {
207    if (segment.startsWith(":")) {
208      return actualSegments[index] !== undefined;
209    }
210
211    return segment === actualSegments[index];
212  });
213}
214
215function normalizePath(path: string): string[] {
216  const normalizedPath = path.replace(/\/+$/u, "");
217  return normalizedPath.split("/").filter((segment) => segment.length > 0);
218}