baa-conductor

git clone 

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

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: "POST",
 40    pathPattern: "/v1/tasks",
 41    action: "tasks.create",
 42    summary: "创建 task"
 43  },
 44  {
 45    method: "POST",
 46    pathPattern: "/v1/tasks/claim",
 47    action: "tasks.claim",
 48    summary: "领取待规划 task 或 runnable step"
 49  },
 50  {
 51    method: "POST",
 52    pathPattern: "/v1/tasks/:task_id/plan",
 53    action: "tasks.plan",
 54    summary: "持久化已验收 plan"
 55  },
 56  {
 57    method: "POST",
 58    pathPattern: "/v1/steps/:step_id/heartbeat",
 59    action: "steps.heartbeat",
 60    summary: "step 心跳"
 61  },
 62  {
 63    method: "POST",
 64    pathPattern: "/v1/steps/:step_id/checkpoint",
 65    action: "steps.checkpoint",
 66    summary: "写 step checkpoint"
 67  },
 68  {
 69    method: "POST",
 70    pathPattern: "/v1/steps/:step_id/complete",
 71    action: "steps.complete",
 72    summary: "标记 step 完成"
 73  },
 74  {
 75    method: "POST",
 76    pathPattern: "/v1/steps/:step_id/fail",
 77    action: "steps.fail",
 78    summary: "标记 step 失败"
 79  },
 80  {
 81    method: "POST",
 82    pathPattern: "/v1/system/pause",
 83    action: "system.pause",
 84    summary: "暂停自动化"
 85  },
 86  {
 87    method: "POST",
 88    pathPattern: "/v1/system/resume",
 89    action: "system.resume",
 90    summary: "恢复自动化"
 91  },
 92  {
 93    method: "POST",
 94    pathPattern: "/v1/system/drain",
 95    action: "system.drain",
 96    summary: "drain 自动化"
 97  },
 98  {
 99    method: "POST",
100    pathPattern: "/v1/system/promote",
101    action: "maintenance.promote",
102    summary: "promote 维护操作"
103  },
104  {
105    method: "POST",
106    pathPattern: "/v1/system/demote",
107    action: "maintenance.demote",
108    summary: "demote 维护操作"
109  },
110  {
111    method: "GET",
112    pathPattern: "/v1/system/state",
113    action: "system.state.read",
114    summary: "读取系统状态"
115  },
116  {
117    method: "GET",
118    pathPattern: "/v1/tasks/:task_id/logs",
119    action: "tasks.logs.read",
120    summary: "读取 task 日志"
121  },
122  {
123    method: "GET",
124    pathPattern: "/v1/tasks/:task_id",
125    action: "tasks.read",
126    summary: "读取 task 详情"
127  },
128  {
129    method: "GET",
130    pathPattern: "/v1/runs/:run_id",
131    action: "runs.read",
132    summary: "读取 run 详情"
133  }
134] satisfies ReadonlyArray<ControlApiAuthRule>;
135
136export function findControlApiAuthRule(
137  method: ControlApiMethod,
138  path: string
139): ControlApiAuthRule | null {
140  for (const rule of CONTROL_API_AUTH_RULES) {
141    if (rule.method === method && matchesRoutePattern(rule.pathPattern, path)) {
142      return rule;
143    }
144  }
145
146  return null;
147}
148
149export function authorizeControlApiRoute(
150  input: ControlApiAuthorizationInput
151): ControlApiAuthorizationResult {
152  const matchedRule = findControlApiAuthRule(input.method, input.path);
153
154  if (!matchedRule) {
155    return {
156      ok: false,
157      reason: "route_not_modeled",
158      statusCode: 404
159    };
160  }
161
162  const decision = authorize({
163    principal: input.principal,
164    action: matchedRule.action,
165    resource: input.resource
166  });
167
168  return {
169    ...decision,
170    matchedRule
171  };
172}
173
174export function describeControlApiAuthSurface(): string[] {
175  return CONTROL_API_AUTH_RULES.map(
176    (rule) => `${rule.method} ${rule.pathPattern} -> ${rule.action} (${rule.summary})`
177  );
178}
179
180function matchesRoutePattern(pattern: string, actualPath: string): boolean {
181  const patternSegments = normalizePath(pattern);
182  const actualSegments = normalizePath(actualPath);
183
184  if (patternSegments.length !== actualSegments.length) {
185    return false;
186  }
187
188  return patternSegments.every((segment, index) => {
189    if (segment.startsWith(":")) {
190      return actualSegments[index] !== undefined;
191    }
192
193    return segment === actualSegments[index];
194  });
195}
196
197function normalizePath(path: string): string[] {
198  const normalizedPath = path.replace(/\/+$/u, "");
199  return normalizedPath.split("/").filter((segment) => segment.length > 0);
200}