@sema-agent/server 7.79.2 → 7.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/boot/late-binds.d.ts +35 -0
  2. package/dist/boot/late-binds.js +20 -0
  3. package/dist/boot/stage-01-config.d.ts +7 -0
  4. package/dist/boot/stage-01-config.js +60 -0
  5. package/dist/boot/stage-02-registry-pull.d.ts +10 -0
  6. package/dist/boot/stage-02-registry-pull.js +11 -0
  7. package/dist/boot/stage-03-store-backend.d.ts +32 -0
  8. package/dist/boot/stage-03-store-backend.js +11 -0
  9. package/dist/boot/stage-04-stores.d.ts +69 -0
  10. package/dist/boot/stage-04-stores.js +49 -0
  11. package/dist/boot/stage-05-execution-env.d.ts +82 -0
  12. package/dist/boot/stage-05-execution-env.js +20 -0
  13. package/dist/boot/stage-06-runners.d.ts +143 -0
  14. package/dist/boot/stage-06-runners.js +207 -0
  15. package/dist/boot/stage-07-capability-layer.d.ts +157 -0
  16. package/dist/boot/stage-07-capability-layer.js +162 -0
  17. package/dist/boot/stage-08-reapers.d.ts +172 -0
  18. package/dist/boot/stage-08-reapers.js +78 -0
  19. package/dist/boot/stage-09-leader.d.ts +204 -0
  20. package/dist/boot/stage-09-leader.js +47 -0
  21. package/dist/boot/stage-10-http-server.d.ts +204 -0
  22. package/dist/boot/stage-10-http-server.js +289 -0
  23. package/dist/config-invariants.d.ts +9 -0
  24. package/dist/config-invariants.js +7 -1
  25. package/dist/config-types.d.ts +1 -1
  26. package/dist/http/dispatch.js +28 -8
  27. package/dist/http/route-ctx.d.ts +101 -4
  28. package/dist/http/route-ctx.js +4 -0
  29. package/dist/http/route-table.d.ts +71 -43
  30. package/dist/http/route-table.js +111 -187
  31. package/dist/http/routes/a2a-serve.d.ts +18 -3
  32. package/dist/http/routes/a2a-serve.js +25 -23
  33. package/dist/http/routes/admin-config-refresh.d.ts +9 -2
  34. package/dist/http/routes/admin-config-refresh.js +32 -31
  35. package/dist/http/routes/admin-drain.d.ts +9 -2
  36. package/dist/http/routes/admin-drain.js +32 -31
  37. package/dist/http/routes/admin-memory-optout.d.ts +28 -2
  38. package/dist/http/routes/admin-memory-optout.js +26 -39
  39. package/dist/http/routes/adoption.d.ts +15 -2
  40. package/dist/http/routes/adoption.js +52 -52
  41. package/dist/http/routes/agents-roster.d.ts +9 -2
  42. package/dist/http/routes/agents-roster.js +82 -80
  43. package/dist/http/routes/approvals-assistant.d.ts +58 -5
  44. package/dist/http/routes/approvals-assistant.js +37 -38
  45. package/dist/http/routes/attachments.d.ts +22 -2
  46. package/dist/http/routes/attachments.js +81 -72
  47. package/dist/http/routes/capabilities.d.ts +21 -2
  48. package/dist/http/routes/capabilities.js +174 -170
  49. package/dist/http/routes/config-catalog.d.ts +9 -2
  50. package/dist/http/routes/config-catalog.js +28 -31
  51. package/dist/http/routes/devices.d.ts +24 -2
  52. package/dist/http/routes/devices.js +19 -21
  53. package/dist/http/routes/diagnostics.d.ts +9 -2
  54. package/dist/http/routes/diagnostics.js +41 -43
  55. package/dist/http/routes/fleet.d.ts +11 -2
  56. package/dist/http/routes/fleet.js +23 -22
  57. package/dist/http/routes/images.d.ts +63 -9
  58. package/dist/http/routes/images.js +53 -46
  59. package/dist/http/routes/leader.d.ts +20 -2
  60. package/dist/http/routes/leader.js +43 -40
  61. package/dist/http/routes/memory-bundle.d.ts +15 -2
  62. package/dist/http/routes/memory-bundle.js +38 -38
  63. package/dist/http/routes/memory-compliance.d.ts +16 -4
  64. package/dist/http/routes/memory-compliance.js +44 -45
  65. package/dist/http/routes/memory-consolidation.d.ts +14 -2
  66. package/dist/http/routes/memory-consolidation.js +66 -66
  67. package/dist/http/routes/memory-origin.d.ts +23 -4
  68. package/dist/http/routes/memory-origin.js +65 -65
  69. package/dist/http/routes/memory-policy.d.ts +21 -2
  70. package/dist/http/routes/memory-policy.js +119 -115
  71. package/dist/http/routes/notify-wake.d.ts +21 -11
  72. package/dist/http/routes/notify-wake.js +118 -116
  73. package/dist/http/routes/observability.d.ts +16 -2
  74. package/dist/http/routes/observability.js +54 -51
  75. package/dist/http/routes/retention-ops.d.ts +19 -2
  76. package/dist/http/routes/retention-ops.js +20 -22
  77. package/dist/http/routes/rules.d.ts +36 -2
  78. package/dist/http/routes/rules.js +22 -24
  79. package/dist/http/routes/run-memory-capture.d.ts +10 -3
  80. package/dist/http/routes/run-memory-capture.js +84 -84
  81. package/dist/http/routes/runs.d.ts +102 -22
  82. package/dist/http/routes/runs.js +71 -88
  83. package/dist/http/routes/session-sync.d.ts +41 -2
  84. package/dist/http/routes/session-sync.js +25 -23
  85. package/dist/http/routes/sessions-list.d.ts +16 -2
  86. package/dist/http/routes/sessions-list.js +78 -75
  87. package/dist/http/routes/sessions.d.ts +77 -2
  88. package/dist/http/routes/sessions.js +50 -47
  89. package/dist/http/routes/shared-memory.d.ts +19 -2
  90. package/dist/http/routes/shared-memory.js +65 -63
  91. package/dist/http/routes/side-query.d.ts +11 -2
  92. package/dist/http/routes/side-query.js +81 -79
  93. package/dist/http/routes/tasks.d.ts +17 -2
  94. package/dist/http/routes/tasks.js +18 -11
  95. package/dist/http/routes/trace-usage.d.ts +40 -2
  96. package/dist/http/routes/trace-usage.js +46 -35
  97. package/dist/http/routes/workflows.d.ts +35 -3
  98. package/dist/http/routes/workflows.js +29 -29
  99. package/dist/main.js +20 -870
  100. package/dist/service-version.d.ts +5 -0
  101. package/dist/service-version.js +12 -0
  102. package/dist/trace/redact.d.ts +7 -6
  103. package/dist/trace/redact.js +419 -229
  104. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { buildReadFaceWiring } from "../../read-face-posture.js";
3
3
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
4
4
  import { sendJson, sendError } from "../send.js";
5
5
  import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
6
+ import { assertNever } from "../route-ctx.js";
6
7
  export function buildServerWiringGates(input) {
7
8
  const gate = resolveStreamApprovalGate({
8
9
  toolApprovalEnabled: input.toolApprovalEnabled,
@@ -78,56 +79,53 @@ export function zeroCredentialLoopbackDenied(remoteAddress, config) {
78
79
  return false;
79
80
  return !isLoopbackAddress(remoteAddress);
80
81
  }
81
- export async function handleDiagnostics(req, res, url, ctx) {
82
- const miss = { fell: false };
83
- await handleDiagnosticsBody(req, res, url, ctx, miss);
84
- return !miss.fell;
85
- }
86
- async function handleDiagnosticsBody(req, res, url, ctx, miss) {
82
+ export async function handleDiagnostics(req, res, match, ctx) {
87
83
  const { deps } = ctx;
88
- if (req.method === "GET" && url === "/v1/diagnostics/wiring") {
89
- if (deps.config.operatorPrincipals.length > 0 || deps.config.requirePrincipal) {
90
- const principal = gatedPrincipal(req, deps.config);
91
- if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
92
- sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only on a multi-tenant worker");
93
- return;
84
+ switch (match.decl.id) {
85
+ case "diagnostics-wiring": {
86
+ if (deps.config.operatorPrincipals.length > 0 || deps.config.requirePrincipal) {
87
+ const principal = gatedPrincipal(req, deps.config);
88
+ if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
89
+ sendError(res, 403, "auth.operator_only", "wiring diagnostics are operator-only on a multi-tenant worker");
90
+ return;
91
+ }
92
+ if (!isLoopbackAddress(req.socket.remoteAddress) && verifiedIdentitySource(req, deps.config) === undefined) {
93
+ sendError(res, 403, "auth.loopback_only", "wiring diagnostics on a zero-credential deployment are readable from loopback only — configure SERVICE_AUTH_TOKEN to read remotely");
94
+ return;
95
+ }
94
96
  }
95
- if (!isLoopbackAddress(req.socket.remoteAddress) && verifiedIdentitySource(req, deps.config) === undefined) {
97
+ else if (zeroCredentialLoopbackDenied(req.socket.remoteAddress, deps.config)) {
96
98
  sendError(res, 403, "auth.loopback_only", "wiring diagnostics on a zero-credential deployment are readable from loopback only — configure SERVICE_AUTH_TOKEN to read remotely");
97
99
  return;
98
100
  }
99
- }
100
- else if (zeroCredentialLoopbackDenied(req.socket.remoteAddress, deps.config)) {
101
- sendError(res, 403, "auth.loopback_only", "wiring diagnostics on a zero-credential deployment are readable from loopback only — configure SERVICE_AUTH_TOKEN to read remotely");
102
- return;
103
- }
104
- if (!deps.staticWiring) {
105
- miss.fell = true;
101
+ sendJson(res, 200, {
102
+ static: deps.staticWiring,
103
+ memoryPosture: deps.memoryPosture ?? null,
104
+ sqlEngine: deps.sqlEngineFacts?.() ?? null,
105
+ writeProtection: deps.writeProtection ?? null,
106
+ readFace: buildReadFaceWiring(deps.config),
107
+ configApply: deps.configApplyState?.() ?? null,
108
+ mcpRevocations: deps.mcpRevocationState?.() ?? null,
109
+ serverGates: buildServerWiringGates({
110
+ toolApprovalEnabled: deps.toolApproval !== undefined,
111
+ streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
112
+ backend: deps.backend,
113
+ checkpointStore: deps.checkpointStore,
114
+ durableApproval: deps.config.durableApproval === true,
115
+ durableApprovalSource: deps.config.durableApprovalSource,
116
+ streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
117
+ streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
118
+ sessionAutoTitle: deps.config.sessionAutoTitle === true,
119
+ sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
120
+ }),
121
+ });
106
122
  return;
107
123
  }
108
- sendJson(res, 200, {
109
- static: deps.staticWiring,
110
- memoryPosture: deps.memoryPosture ?? null,
111
- sqlEngine: deps.sqlEngineFacts?.() ?? null,
112
- writeProtection: deps.writeProtection ?? null,
113
- readFace: buildReadFaceWiring(deps.config),
114
- configApply: deps.configApplyState?.() ?? null,
115
- mcpRevocations: deps.mcpRevocationState?.() ?? null,
116
- serverGates: buildServerWiringGates({
117
- toolApprovalEnabled: deps.toolApproval !== undefined,
118
- streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
119
- backend: deps.backend,
120
- checkpointStore: deps.checkpointStore,
121
- durableApproval: deps.config.durableApproval === true,
122
- durableApprovalSource: deps.config.durableApprovalSource,
123
- streamAskWindowMs: deps.config.streamApproval?.windowMs ?? 0,
124
- streamAskWindowMsSource: deps.config.streamAskWindowMsSource,
125
- sessionAutoTitle: deps.config.sessionAutoTitle === true,
126
- sessionAutoTitleSource: deps.config.sessionAutoTitleSource,
127
- }),
128
- });
129
- return;
124
+ default:
125
+ return assertNever(match.decl.id);
130
126
  }
131
- miss.fell = true;
132
127
  }
128
+ export const DIAGNOSTICS_ROUTES = [
129
+ { id: "diagnostics-wiring", path: "/v1/diagnostics/wiring", methods: ["GET"] },
130
+ ];
133
131
  //# sourceMappingURL=diagnostics.js.map
@@ -5,6 +5,15 @@
5
5
  * (按 principal 自门,与 workflow 只读路由同列)。本域零可变状态。
6
6
  */
7
7
  import type { IncomingMessage, ServerResponse } from "node:http";
8
- import type { RouteCtx } from "../route-ctx.js";
9
- export declare function handleFleet(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
8
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
9
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
10
+ * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
11
+ export declare function handleFleet(req: IncomingMessage, res: ServerResponse, match: RouteMatch<FleetRouteId>, ctx: RouteCtx): Promise<void>;
12
+ export declare const FLEET_ROUTES: readonly [{
13
+ readonly id: "fleet-stream";
14
+ readonly path: "/v1/fleet/stream";
15
+ readonly methods: readonly ["GET"];
16
+ }];
17
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
18
+ export type FleetRouteId = RouteIdsOf<typeof FLEET_ROUTES>;
10
19
  //# sourceMappingURL=fleet.d.ts.map
@@ -3,33 +3,31 @@ import { FLEET_SNAPSHOT_TERMINAL_MAX_ROWS, isTerminalWorkflowRowStatus, recentTe
3
3
  import { taskNotificationFoldKey } from "../../orchestration/workflow-completion-inbox.js";
4
4
  import { sendJson, sendError, sseHeaders } from "../send.js";
5
5
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
6
- export async function handleFleet(req, res, url, ctx) {
7
- const miss = { fell: false };
8
- await handleFleetBody(req, res, url, ctx, miss);
9
- return !miss.fell;
10
- }
11
- async function handleFleetBody(req, res, url, ctx, miss) {
6
+ import { assertNever } from "../route-ctx.js";
7
+ export async function handleFleet(req, res, match, ctx) {
12
8
  const { deps } = ctx;
13
9
  const { sessionOwnerScope } = ctx.helpers;
14
- void url;
15
- if (req.method === "GET" && new URL(req.url ?? "", "http://x").pathname === "/v1/fleet/stream") {
16
- if (!deps.fleetBus) {
17
- sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
18
- return;
19
- }
20
- const { fleetWide } = sessionOwnerScope(req);
21
- const principal = gatedPrincipal(req, deps.config);
22
- if (deps.config.requirePrincipal && !principal && !fleetWide) {
23
- sendPrincipalRefusal(res, req, deps.config);
10
+ switch (match.decl.id) {
11
+ case "fleet-stream": {
12
+ if (!deps.fleetBus) {
13
+ sendError(res, 501, "capability.self_orchestration_required", "fleet stream requires self-orchestration / fleet wiring");
14
+ return;
15
+ }
16
+ const { fleetWide } = sessionOwnerScope(req);
17
+ const principal = gatedPrincipal(req, deps.config);
18
+ if (deps.config.requirePrincipal && !principal && !fleetWide) {
19
+ sendPrincipalRefusal(res, req, deps.config);
20
+ return;
21
+ }
22
+ const fleetParams = new URLSearchParams(match.search);
23
+ const callerSession = fleetParams.get("session");
24
+ const observeOnly = fleetParams.get("observe") === "1";
25
+ await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
24
26
  return;
25
27
  }
26
- const fleetParams = new URL(req.url ?? "", "http://x").searchParams;
27
- const callerSession = fleetParams.get("session");
28
- const observeOnly = fleetParams.get("observe") === "1";
29
- await streamFleet(req, res, deps.fleetBus, fleetWide ? null : (principal ?? "default"), callerSession || null, observeOnly ? undefined : deps.workflowCompletionInbox, { store: deps.workflowRunStore, scope: fleetWide ? null : (principal ?? "default"), pullScope: principal ?? "default", ready: deps.fleetRecoveryDone });
30
- return;
28
+ default:
29
+ return assertNever(match.decl.id);
31
30
  }
32
- miss.fell = true;
33
31
  }
34
32
  function streamFleet(req, res, bus, callerScope, callerSession = null, completionInbox, terminal) {
35
33
  return new Promise((resolve) => {
@@ -239,4 +237,7 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
239
237
  })().catch(() => shutdown());
240
238
  });
241
239
  }
240
+ export const FLEET_ROUTES = [
241
+ { id: "fleet-stream", path: "/v1/fleet/stream", methods: ["GET"] },
242
+ ];
242
243
  //# sourceMappingURL=fleet.js.map
@@ -13,7 +13,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
13
13
  import { RateLimiter } from "../../observability/rate-limit.js";
14
14
  import type { BakeState, BakeErrorCode } from "../../plugins/store-contracts.js";
15
15
  import type { FlatServiceDeps } from "../server.js";
16
- import type { RouteCtx } from "../route-ctx.js";
16
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
17
17
  /** 单域独占状态(lens1 §C2 的 12 条之一)。每个 server 实例一份——模块级会把限速状态跨实例串味
18
18
  * (一个进程里建几十个 server 的测试会互相污染)。 */
19
19
  export interface ImagesLocal {
@@ -26,12 +26,66 @@ export declare function coarseStatusForState(state: BakeState): "running" | "don
26
26
  /** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
27
27
  * uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
28
28
  export declare function errorCodeForExit(exitCode: number | null | undefined, cancelled: boolean): BakeErrorCode;
29
- export declare const IMAGE_DIGEST_RE: RegExp;
30
- export declare const IMAGE_PROFILE_RE: RegExp;
31
- export declare const BAKE_ID_RE: RegExp;
32
- export declare const BAKE_EVENTS_RE: RegExp;
33
- export declare const BAKE_CANCEL_RE: RegExp;
34
- export declare const BAKE_CLAIM_RE: RegExp;
35
- export declare const BAKE_INGEST_RE: RegExp;
36
- export declare function handleImages(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
29
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
30
+ * 的语义原样 =「本域已应答」;走到函数尾才是「没匹配上」,置 `miss.fell` 交回装配器继续下一个域。
31
+ * 跨域的东西(deps / 助手 / 单域状态 / 每请求量)一律在首行解构出来,于是下面每一行都逐字不变。 */
32
+ export declare function handleImages(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ImagesRouteId>, ctx: RouteCtx): Promise<void>;
33
+ export declare const IMAGES_ROUTES: readonly [{
34
+ readonly id: "images-index";
35
+ readonly path: "/v1/images";
36
+ readonly methods: readonly ["GET"];
37
+ }, {
38
+ readonly id: "image-bake-submit";
39
+ readonly path: "/v1/images/bakes";
40
+ readonly methods: readonly ["POST"];
41
+ }, {
42
+ readonly id: "image-bake-claim-next";
43
+ readonly path: "/v1/images/bakes/claim";
44
+ readonly methods: readonly ["POST"];
45
+ }, {
46
+ readonly id: "image-select";
47
+ readonly path: "/v1/images/select";
48
+ readonly methods: readonly ["POST"];
49
+ }, {
50
+ readonly id: "image-register";
51
+ readonly path: "/v1/images/register";
52
+ readonly methods: readonly ["POST"];
53
+ }, {
54
+ readonly id: "image-bake-events";
55
+ readonly pattern: RegExp;
56
+ readonly label: "/v1/images/bakes/:id/events";
57
+ readonly methods: readonly ["GET"];
58
+ }, {
59
+ readonly id: "image-bake-cancel";
60
+ readonly pattern: RegExp;
61
+ readonly label: "/v1/images/bakes/:id/cancel";
62
+ readonly methods: readonly ["POST"];
63
+ }, {
64
+ readonly id: "image-bake-claim";
65
+ readonly pattern: RegExp;
66
+ readonly label: "/v1/images/bakes/:id/claim";
67
+ readonly methods: readonly ["POST"];
68
+ }, {
69
+ readonly id: "image-bake-ingest";
70
+ readonly pattern: RegExp;
71
+ readonly label: "/v1/images/bakes/:id/ingest";
72
+ readonly methods: readonly ["POST"];
73
+ }, {
74
+ readonly id: "image-bake-detail";
75
+ readonly pattern: RegExp;
76
+ readonly label: "/v1/images/bakes/:id";
77
+ readonly methods: readonly ["GET"];
78
+ }, {
79
+ readonly id: "image-by-digest";
80
+ readonly pattern: RegExp;
81
+ readonly label: "/v1/images/digests/:digest";
82
+ readonly methods: readonly ["GET"];
83
+ }, {
84
+ readonly id: "image-by-profile";
85
+ readonly pattern: RegExp;
86
+ readonly label: "/v1/images/:profile";
87
+ readonly methods: readonly ["GET"];
88
+ }];
89
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
90
+ export type ImagesRouteId = RouteIdsOf<typeof IMAGES_ROUTES>;
37
91
  //# sourceMappingURL=images.d.ts.map
@@ -7,6 +7,7 @@ import { streamSseLog } from "../sse-log.js";
7
7
  import { sendJson, sendError } from "../send.js";
8
8
  import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
9
9
  import { scopedIdempotencyKey } from "../idempotency.js";
10
+ import { assertNever } from "../route-ctx.js";
10
11
  export function createImagesLocal(deps) {
11
12
  return { bakeSubmitLimiter: new RateLimiter(deps.config.imageBakes?.submitRateMax ?? 0, (deps.config.imageBakes?.submitRateWindowSec ?? 60) * 1000) };
12
13
  }
@@ -38,24 +39,12 @@ export function errorCodeForExit(exitCode, cancelled) {
38
39
  }
39
40
  const RUNNER_ERROR_CODES = new Set(["band-conflict", "duplicate", "disk", "timeout", "cancelled"]);
40
41
  const SANDBOX_BASE_PROFILE = "sandbox-base";
41
- export const IMAGE_DIGEST_RE = /^\/v1\/images\/digests\/([^/]+)$/;
42
- export const IMAGE_PROFILE_RE = /^\/v1\/images\/([^/]+)$/;
43
- export const BAKE_ID_RE = /^\/v1\/images\/bakes\/([^/]+)$/;
44
- export const BAKE_EVENTS_RE = /^\/v1\/images\/bakes\/([^/]+)\/events$/;
45
- export const BAKE_CANCEL_RE = /^\/v1\/images\/bakes\/([^/]+)\/cancel$/;
46
- export const BAKE_CLAIM_RE = /^\/v1\/images\/bakes\/([^/]+)\/claim$/;
47
- export const BAKE_INGEST_RE = /^\/v1\/images\/bakes\/([^/]+)\/ingest$/;
48
- export async function handleImages(req, res, url, ctx) {
49
- const miss = { fell: false };
50
- await handleImagesBody(req, res, url, ctx, miss);
51
- return !miss.fell;
52
- }
53
- async function handleImagesBody(req, res, url, ctx, miss) {
42
+ export async function handleImages(req, res, match, ctx) {
54
43
  const { deps } = ctx;
55
44
  const { readJson } = ctx.helpers;
56
45
  const { source } = ctx.req;
57
46
  const { bakeSubmitLimiter } = ctx.local.images;
58
- if (deps.imageBakes && url.startsWith("/v1/images/bakes")) {
47
+ if (deps.imageBakes && isBakeRouteId(match.decl.id)) {
59
48
  const bakes = deps.imageBakes;
60
49
  const idx = deps.imageIndex;
61
50
  const principal = gatedPrincipal(req, deps.config);
@@ -63,7 +52,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
63
52
  const isExplicitOperator = explicitOperatorOk(principal, operators);
64
53
  const cfg = deps.config.imageBakes;
65
54
  const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
66
- if (req.method === "POST" && url === "/v1/images/bakes") {
55
+ if (match.decl.id === "image-bake-submit") {
67
56
  if (!isExplicitOperator) {
68
57
  sendError(res, 403, "auth.operator_only", "operator only");
69
58
  return;
@@ -134,13 +123,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
134
123
  sendJson(res, 202, bakeAcceptedBody(bake));
135
124
  return;
136
125
  }
137
- const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
138
- if (evMatch) {
126
+ if (match.decl.id === "image-bake-events") {
139
127
  if (!isExplicitOperator) {
140
128
  sendError(res, 403, "auth.operator_only", "operator only");
141
129
  return;
142
130
  }
143
- const bakeId = decodeURIComponent(evMatch[1]);
131
+ const bakeId = decodeURIComponent(match.params.bakeId);
144
132
  const bake = await bakes.getBake(bakeId);
145
133
  if (!bake) {
146
134
  sendError(res, 404, "not_found.bake", "bake not found");
@@ -149,13 +137,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
149
137
  await streamBakeEvents(req, res, bakes, bakeId, cfg.staleMs);
150
138
  return;
151
139
  }
152
- const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
153
- if (cancelMatch) {
140
+ if (match.decl.id === "image-bake-cancel") {
154
141
  if (!isExplicitOperator) {
155
142
  sendError(res, 403, "auth.operator_only", "operator only");
156
143
  return;
157
144
  }
158
- const bakeId = decodeURIComponent(cancelMatch[1]);
145
+ const bakeId = decodeURIComponent(match.params.bakeId);
159
146
  const bake = await bakes.getBake(bakeId);
160
147
  if (!bake) {
161
148
  sendError(res, 404, "not_found.bake", "bake not found");
@@ -165,7 +152,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
165
152
  sendJson(res, 202, { bakeId, cancelRequested: true, note: flagged ? "cancel requested — the runner kills the build at the next heartbeat" : "bake already terminal — cancel is a no-op" });
166
153
  return;
167
154
  }
168
- if (req.method === "POST" && url === "/v1/images/bakes/claim") {
155
+ if (match.decl.id === "image-bake-claim-next") {
169
156
  if (!isRunner) {
170
157
  sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
171
158
  return;
@@ -186,13 +173,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
186
173
  sendJson(res, 200, claimResponse(claimed));
187
174
  return;
188
175
  }
189
- const claimMatch = req.method === "POST" ? BAKE_CLAIM_RE.exec(url) : null;
190
- if (claimMatch) {
176
+ if (match.decl.id === "image-bake-claim") {
191
177
  if (!isRunner) {
192
178
  sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
193
179
  return;
194
180
  }
195
- const bakeId = decodeURIComponent(claimMatch[1]);
181
+ const bakeId = decodeURIComponent(match.params.bakeId);
196
182
  const body = (await readJson(req));
197
183
  const runnerId = typeof body.runnerId === "string" && body.runnerId.length > 0 ? body.runnerId : (principal ?? source ?? cfg.runnerPrincipal);
198
184
  const claimed = await bakes.claimBake(bakeId, runnerId, cfg.leaseMs);
@@ -203,13 +189,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
203
189
  sendJson(res, 200, claimResponse(claimed));
204
190
  return;
205
191
  }
206
- const ingestMatch = req.method === "POST" ? BAKE_INGEST_RE.exec(url) : null;
207
- if (ingestMatch) {
192
+ if (match.decl.id === "image-bake-ingest") {
208
193
  if (!isRunner) {
209
194
  sendError(res, 403, "auth.bake_runner_only", "bake-runner only");
210
195
  return;
211
196
  }
212
- const bakeId = decodeURIComponent(ingestMatch[1]);
197
+ const bakeId = decodeURIComponent(match.params.bakeId);
213
198
  const bake = await bakes.getBake(bakeId);
214
199
  if (!bake) {
215
200
  sendError(res, 404, "not_found.bake", "bake not found");
@@ -248,13 +233,12 @@ async function handleImagesBody(req, res, url, ctx, miss) {
248
233
  sendJson(res, result.status, { ...result.body, cancelRequested: after?.cancelRequested ?? false, leaseValid: true });
249
234
  return;
250
235
  }
251
- const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
252
- if (idMatch) {
236
+ if (match.decl.id === "image-bake-detail") {
253
237
  if (!isExplicitOperator) {
254
238
  sendError(res, 403, "auth.operator_only", "operator only");
255
239
  return;
256
240
  }
257
- const bakeId = decodeURIComponent(idMatch[1]);
241
+ const bakeId = decodeURIComponent(match.params.bakeId);
258
242
  const bake = await bakes.getBake(bakeId);
259
243
  if (!bake) {
260
244
  sendError(res, 404, "not_found.bake", "bake not found");
@@ -263,16 +247,15 @@ async function handleImagesBody(req, res, url, ctx, miss) {
263
247
  sendJson(res, 200, { bake: bakeView(bake) });
264
248
  return;
265
249
  }
266
- sendError(res, 404, "not_found.route", "not found");
267
- return;
250
+ return assertNever(match.decl.id);
268
251
  }
269
- if (deps.imageIndex && url.startsWith("/v1/images")) {
252
+ if (deps.imageIndex && isIndexRouteId(match.decl.id)) {
270
253
  const idx = deps.imageIndex;
271
254
  const principal = gatedPrincipal(req, deps.config);
272
255
  const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
273
256
  const viewer = { operator, tenantId: principal ?? null };
274
- const q = new URL(req.url ?? "", "http://x").searchParams;
275
- if (req.method === "GET" && url === "/v1/images") {
257
+ const q = new URLSearchParams(match.search);
258
+ if (match.decl.id === "images-index") {
276
259
  const limRaw = q.get("limit");
277
260
  const { entries, nextCursor } = await idx.list({
278
261
  ...viewer,
@@ -286,7 +269,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
286
269
  sendJson(res, 200, { images: entries, nextCursor });
287
270
  return;
288
271
  }
289
- if (req.method === "POST" && url === "/v1/images/select") {
272
+ if (match.decl.id === "image-select") {
290
273
  const body = (await readJson(req));
291
274
  const profile = typeof body.profile === "string" ? body.profile : "";
292
275
  if (!profile) {
@@ -318,7 +301,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
318
301
  });
319
302
  return;
320
303
  }
321
- if (req.method === "POST" && url === "/v1/images/register") {
304
+ if (match.decl.id === "image-register") {
322
305
  if (!explicitOperatorOk(principal, deps.config.operatorPrincipals)) {
323
306
  sendError(res, 403, "auth.operator_only", "operator only");
324
307
  return;
@@ -351,9 +334,8 @@ async function handleImagesBody(req, res, url, ctx, miss) {
351
334
  sendJson(res, 200, { id });
352
335
  return;
353
336
  }
354
- const digMatch = req.method === "GET" ? IMAGE_DIGEST_RE.exec(url) : null;
355
- if (digMatch) {
356
- const entry = await idx.getByDigest(decodeURIComponent(digMatch[1]));
337
+ if (match.decl.id === "image-by-digest") {
338
+ const entry = await idx.getByDigest(decodeURIComponent(match.params.digest));
357
339
  if (!entry || (!viewer.operator && entry.visibility === "tenant" && entry.tenantId !== viewer.tenantId)) {
358
340
  sendError(res, 404, "not_found.image", "not found");
359
341
  return;
@@ -361,9 +343,8 @@ async function handleImagesBody(req, res, url, ctx, miss) {
361
343
  sendJson(res, 200, { image: entry });
362
344
  return;
363
345
  }
364
- const profMatch = req.method === "GET" ? IMAGE_PROFILE_RE.exec(url) : null;
365
- if (profMatch) {
366
- const entry = await idx.latestPublished(decodeURIComponent(profMatch[1]), viewer);
346
+ if (match.decl.id === "image-by-profile") {
347
+ const entry = await idx.latestPublished(decodeURIComponent(match.params.profile), viewer);
367
348
  if (!entry) {
368
349
  sendError(res, 404, "not_found.image", "not found");
369
350
  return;
@@ -371,13 +352,25 @@ async function handleImagesBody(req, res, url, ctx, miss) {
371
352
  sendJson(res, 200, { image: entry });
372
353
  return;
373
354
  }
355
+ return assertNever(match.decl.id);
374
356
  }
375
- else if (url === "/v1/images" || url.startsWith("/v1/images/")) {
357
+ {
376
358
  sendError(res, 501, "capability.image_index_required", "the sandbox-image-pool requires a SQL store backend (DB_BACKEND=mysql|pg) — the local backend wires no image index");
377
359
  return;
378
360
  }
379
- miss.fell = true;
380
361
  }
362
+ const BAKE_ROUTE_IDS = [
363
+ "image-bake-submit",
364
+ "image-bake-claim-next",
365
+ "image-bake-events",
366
+ "image-bake-cancel",
367
+ "image-bake-claim",
368
+ "image-bake-ingest",
369
+ "image-bake-detail",
370
+ ];
371
+ const BAKE_ROUTE_ID_SET = new Set(BAKE_ROUTE_IDS);
372
+ const isBakeRouteId = (id) => BAKE_ROUTE_ID_SET.has(id);
373
+ const isIndexRouteId = (id) => !BAKE_ROUTE_ID_SET.has(id);
381
374
  async function streamBakeEvents(req, res, bakes, bakeId, staleMs) {
382
375
  await streamSseLog(req, res, {
383
376
  statusOf: async (id) => {
@@ -576,4 +569,18 @@ async function ingestBakeLine(bakes, idx, bake, line, registry) {
576
569
  }
577
570
  return { status: 200, body: { accepted: true, terminal: affectedRows >= 1, indexId } };
578
571
  }
572
+ export const IMAGES_ROUTES = [
573
+ { id: "images-index", path: "/v1/images", methods: ["GET"] },
574
+ { id: "image-bake-submit", path: "/v1/images/bakes", methods: ["POST"] },
575
+ { id: "image-bake-claim-next", path: "/v1/images/bakes/claim", methods: ["POST"] },
576
+ { id: "image-select", path: "/v1/images/select", methods: ["POST"] },
577
+ { id: "image-register", path: "/v1/images/register", methods: ["POST"] },
578
+ { id: "image-bake-events", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/events$/, label: "/v1/images/bakes/:id/events", methods: ["GET"] },
579
+ { id: "image-bake-cancel", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/cancel$/, label: "/v1/images/bakes/:id/cancel", methods: ["POST"] },
580
+ { id: "image-bake-claim", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/claim$/, label: "/v1/images/bakes/:id/claim", methods: ["POST"] },
581
+ { id: "image-bake-ingest", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)\/ingest$/, label: "/v1/images/bakes/:id/ingest", methods: ["POST"] },
582
+ { id: "image-bake-detail", pattern: /^\/v1\/images\/bakes\/(?<bakeId>[^/]+)$/, label: "/v1/images/bakes/:id", methods: ["GET"] },
583
+ { id: "image-by-digest", pattern: /^\/v1\/images\/digests\/(?<digest>[^/]+)$/, label: "/v1/images/digests/:digest", methods: ["GET"] },
584
+ { id: "image-by-profile", pattern: /^\/v1\/images\/(?<profile>[^/]+)$/, label: "/v1/images/:profile", methods: ["GET"] },
585
+ ];
579
586
  //# sourceMappingURL=images.js.map
@@ -6,6 +6,24 @@
6
6
  * 任何持服务令牌者能提交/读任意租户的 leader run),owner 用 crypto 验证过的 `gatedPrincipal`。本域零可变状态。
7
7
  */
8
8
  import type { IncomingMessage, ServerResponse } from "node:http";
9
- import type { RouteCtx } from "../route-ctx.js";
10
- export declare function handleLeader(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;
9
+ import type { RouteCtx, RouteMatch, RouteIdsOf } from "../route-ctx.js";
10
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。
11
+ * 注意本域的落空语义:`deps.leaderEndpoint` 在但 URL 不匹配、或 endpoint 返回 undefined,都要**继续**
12
+ * 往下走(原文的 if 块不 return),故只有真的应答过才算已处理——由末尾的 `miss.fell` 如实表达。 */
13
+ /** 本域两条路径的**单一属主**(下面的声明行与那只接缝的回拼都引它,不再各写一遍字面量)。 */
14
+ export declare const LEADER_SUBMIT_PATH = "/v1/leader";
15
+ export declare function handleLeader(req: IncomingMessage, res: ServerResponse, match: RouteMatch<LeaderRouteId>, ctx: RouteCtx): Promise<void>;
16
+ export declare const LEADER_ROUTES: readonly [{
17
+ readonly id: "leader-submit";
18
+ readonly path: "/v1/leader";
19
+ readonly billable: true;
20
+ readonly methods: readonly ["POST"];
21
+ }, {
22
+ readonly id: "leader-detail";
23
+ readonly pattern: RegExp;
24
+ readonly label: "/v1/leader/:id";
25
+ readonly methods: readonly ["GET"];
26
+ }];
27
+ /** 本域可分派行的 `id` 闭集 —— handler 的 `switch` 按它判穷尽(漏一口 = 编译红)。 */
28
+ export type LeaderRouteId = RouteIdsOf<typeof LEADER_ROUTES>;
11
29
  //# sourceMappingURL=leader.d.ts.map
@@ -1,48 +1,51 @@
1
1
  import { sendJson, sendError } from "../send.js";
2
2
  import { gatedPrincipal, headerStr, sendPrincipalRefusal } from "../principal-gate.js";
3
- export async function handleLeader(req, res, url, ctx) {
4
- const miss = { fell: false };
5
- await handleLeaderBody(req, res, url, ctx, miss);
6
- return !miss.fell;
7
- }
8
- async function handleLeaderBody(req, res, url, ctx, miss) {
3
+ import { assertNever } from "../route-ctx.js";
4
+ export const LEADER_SUBMIT_PATH = "/v1/leader";
5
+ export async function handleLeader(req, res, match, ctx) {
6
+ switch (match.decl.id) {
7
+ case "leader-submit":
8
+ case "leader-detail":
9
+ break;
10
+ default:
11
+ return assertNever(match.decl.id);
12
+ }
9
13
  const { deps } = ctx;
10
14
  const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
11
- if (deps.leaderEndpoint) {
12
- const isLeaderPost = req.method === "POST" && url === "/v1/leader";
13
- const isLeaderGet = req.method === "GET" && /^\/v1\/leader\/[^/]+$/.test(url);
14
- if (isLeaderPost || isLeaderGet) {
15
- if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
16
- sendPrincipalRefusal(res, req, deps.config);
17
- return;
18
- }
19
- const requester = gatedPrincipal(req, deps.config) ?? null;
20
- if (isLeaderPost) {
21
- if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
22
- return;
23
- let body;
24
- try {
25
- body = await readJson(req);
26
- }
27
- catch {
28
- sendError(res, 400, "request.invalid_json", "invalid JSON body");
29
- return;
30
- }
31
- const r = await deps.leaderEndpoint.handle("POST", url, body, requester, headerStr(req.headers["idempotency-key"]) ?? null);
32
- if (r) {
33
- sendJson(res, r.status, r.body);
34
- return;
35
- }
36
- }
37
- else {
38
- const r = await deps.leaderEndpoint.handle("GET", url, undefined, requester);
39
- if (r) {
40
- sendJson(res, r.status, r.body);
41
- return;
42
- }
43
- }
15
+ const endpoint = deps.leaderEndpoint;
16
+ if (deps.config.requirePrincipal && !gatedPrincipal(req, deps.config)) {
17
+ sendPrincipalRefusal(res, req, deps.config);
18
+ return;
19
+ }
20
+ const requester = gatedPrincipal(req, deps.config) ?? null;
21
+ if (match.decl.id === "leader-submit") {
22
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
23
+ return;
24
+ let body;
25
+ try {
26
+ body = await readJson(req);
27
+ }
28
+ catch {
29
+ sendError(res, 400, "request.invalid_json", "invalid JSON body");
30
+ return;
31
+ }
32
+ const r = await endpoint.handle("POST", LEADER_SUBMIT_PATH, body, requester, headerStr(req.headers["idempotency-key"]) ?? null);
33
+ if (r) {
34
+ sendJson(res, r.status, r.body);
35
+ return;
36
+ }
37
+ }
38
+ else {
39
+ const r = await endpoint.handle("GET", `${LEADER_SUBMIT_PATH}/${match.params.id}`, undefined, requester);
40
+ if (r) {
41
+ sendJson(res, r.status, r.body);
42
+ return;
44
43
  }
45
44
  }
46
- miss.fell = true;
45
+ throw new Error(`leader endpoint did not claim its own declared route: ${match.decl.id}`);
47
46
  }
47
+ export const LEADER_ROUTES = [
48
+ { id: "leader-submit", path: LEADER_SUBMIT_PATH, billable: true, methods: ["POST"] },
49
+ { id: "leader-detail", pattern: /^\/v1\/leader\/(?<id>[^/]+)$/, label: "/v1/leader/:id", methods: ["GET"] },
50
+ ];
48
51
  //# sourceMappingURL=leader.js.map