@code-partner/codepipe-hub 0.14.1-dev.422.gf90f06c4 → 0.14.1-dev.424.ge3c8cc77

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.
package/hub/dist/index.js CHANGED
@@ -13,7 +13,7 @@ var PLATFORM_VERSION;
13
13
  var init_version_generated = __esm({
14
14
  "../packages/shared/dist/version.generated.js"() {
15
15
  "use strict";
16
- PLATFORM_VERSION = "0.14.1-dev.422.gf90f06c4";
16
+ PLATFORM_VERSION = "0.14.1-dev.424.ge3c8cc77";
17
17
  }
18
18
  });
19
19
 
@@ -31,8 +31,8 @@ var init_version = __esm({
31
31
  "../packages/shared/dist/version.js"() {
32
32
  "use strict";
33
33
  init_version_generated();
34
- PROTOCOL_VERSION = 4;
35
- MIN_SUPPORTED_PROTOCOL = 4;
34
+ PROTOCOL_VERSION = 5;
35
+ MIN_SUPPORTED_PROTOCOL = 5;
36
36
  }
37
37
  });
38
38
 
@@ -2960,21 +2960,14 @@ var init_duration = __esm({
2960
2960
  });
2961
2961
 
2962
2962
  // ../packages/shared/dist/node-handshake.js
2963
- function readNodeSocketSecrets(headers, query) {
2964
- const found = [];
2963
+ function readNodeSocketSecret(headers) {
2965
2964
  const raw = headers["authorization"];
2966
2965
  const header = Array.isArray(raw) ? raw[0] : raw;
2967
- if (typeof header === "string") {
2968
- const match = /^Bearer\s+(.+)$/i.exec(header.trim());
2969
- const token = match?.[1]?.trim();
2970
- if (token !== void 0 && token !== "")
2971
- found.push({ token, source: "header" });
2972
- }
2973
- const fromQuery = query?.token;
2974
- if (typeof fromQuery === "string" && fromQuery !== "") {
2975
- found.push({ token: fromQuery, source: "query" });
2976
- }
2977
- return found;
2966
+ if (typeof header !== "string")
2967
+ return null;
2968
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
2969
+ const token = match?.[1]?.trim();
2970
+ return token !== void 0 && token !== "" ? token : null;
2978
2971
  }
2979
2972
  function decideNodeHandshake(frame, hub, recognizedCapabilities, authenticatedAs) {
2980
2973
  const handshake = parseNodeHandshake(frame);
@@ -8682,7 +8675,10 @@ var init_route_definition = __esm({
8682
8675
  node: ["/node/v1"],
8683
8676
  local: ["/local"],
8684
8677
  mcp: ["/mcp"],
8685
- bootstrap: ["/auth", "/bootstrap"],
8678
+ // `/cli` is the CLI's own bootstrap: the connection request it opens, the
8679
+ // page and the email link a person confirms it on (DEV-927 settled it here
8680
+ // rather than moving a path the CLI and a sent email already hold).
8681
+ bootstrap: ["/auth", "/bootstrap", "/cli"],
8686
8682
  operational: ["/healthz", "/readyz"],
8687
8683
  // Static serves the browser bundle from the root and the install script from
8688
8684
  // its documented path (ADR-0060 §5), so it has no single prefix to check.
@@ -8704,7 +8700,8 @@ var init_route_definition = __esm({
8704
8700
  "/api/tasks",
8705
8701
  "/api/projects",
8706
8702
  "/api/admin",
8707
- "/api/notifications"
8703
+ "/api/notifications",
8704
+ "/bug-report"
8708
8705
  ];
8709
8706
  PUBLIC_MUTATION_ALLOWLIST = [
8710
8707
  "auth.login",
@@ -8712,18 +8709,20 @@ var init_route_definition = __esm({
8712
8709
  "auth.loginPassword",
8713
8710
  "auth.logout",
8714
8711
  "auth.register",
8715
- "auth.setPassword",
8716
8712
  "bootstrap.request",
8717
- "bootstrap.confirm",
8718
8713
  "bootstrap.authorizeEmail",
8719
- "bootstrap.authorizeConfirm"
8714
+ // DEV-927: a node registers with a registration token in the body — the
8715
+ // token IS the credential, and there is no node identity yet to bear it.
8716
+ "node.sandbox.register",
8717
+ "node.farm.register",
8718
+ "node.runner.register"
8720
8719
  ];
8721
8720
  }
8722
8721
  });
8723
8722
 
8724
8723
  // src/http/route-registry.ts
8725
- function installRouteGuard(app, mode, log) {
8726
- const registry = new RouteRegistry(mode, log);
8724
+ function installRouteGuard(app, mode, log, legacy = []) {
8725
+ const registry = new RouteRegistry(mode, log, legacy);
8727
8726
  app.addHook("onRoute", (route) => registry.inspect(route));
8728
8727
  return registry;
8729
8728
  }
@@ -8744,15 +8743,18 @@ var init_route_registry = __esm({
8744
8743
  init_route_definition();
8745
8744
  META_KEY = "codepipeRoute";
8746
8745
  RouteRegistry = class {
8747
- constructor(mode, log = { warn: (message) => console.warn(message) }) {
8746
+ constructor(mode, log = { warn: (message) => console.warn(message) }, legacy = []) {
8748
8747
  this.mode = mode;
8749
8748
  this.log = log;
8749
+ this.legacyAllowed = new Set(legacy);
8750
8750
  }
8751
8751
  routes = /* @__PURE__ */ new Map();
8752
8752
  paths = /* @__PURE__ */ new Set();
8753
8753
  operationIds = /* @__PURE__ */ new Set();
8754
8754
  unclassified = 0;
8755
8755
  invalid = 0;
8756
+ legacyAllowed;
8757
+ legacySeen = /* @__PURE__ */ new Set();
8756
8758
  /** Every route registered with metadata, in registration order. */
8757
8759
  list() {
8758
8760
  return [...this.routes.values()];
@@ -8770,6 +8772,12 @@ var init_route_registry = __esm({
8770
8772
  unclassifiedCount() {
8771
8773
  return this.unclassified;
8772
8774
  }
8775
+ /** The same registrations by name (`METHOD /path`), so a test can hold the
8776
+ * legacy list to exactly what the process still serves: an entry nobody
8777
+ * registers any more is a stale allowance, and it fails there. */
8778
+ unclassifiedRoutes() {
8779
+ return [...this.legacySeen];
8780
+ }
8773
8781
  /** Registrations that had metadata and broke a rule. Counted separately from
8774
8782
  * the unclassified ones: "not migrated yet" and "migrated wrongly" are
8775
8783
  * different problems, and a violation that lands in neither count is a
@@ -8789,12 +8797,14 @@ var init_route_registry = __esm({
8789
8797
  if (methods.length === 0) return;
8790
8798
  this.paths.add(route.url);
8791
8799
  if (!meta) {
8792
- this.unclassified += 1;
8793
- if (this.mode === "enforce") {
8800
+ const tolerated = methods.every((method) => this.legacyAllowed.has(`${method} ${route.url}`));
8801
+ if (this.mode === "enforce" && !tolerated) {
8794
8802
  throw new Error(
8795
- `route registry: ${methods.join("/")} ${route.url} registered without metadata \u2014 every route declares its surface, operationId and policy (ADR-0060 \xA73)`
8803
+ `route registry: ${methods.join("/")} ${route.url} registered without metadata \u2014 every route declares its surface, operationId and policy (ADR-0060 \xA73); the old roots are closed since the cutover (DEV-927)`
8796
8804
  );
8797
8805
  }
8806
+ this.unclassified += 1;
8807
+ for (const method of methods) this.legacySeen.add(`${method} ${route.url}`);
8798
8808
  return;
8799
8809
  }
8800
8810
  const violations = methods.flatMap(
@@ -12239,6 +12249,8 @@ function createCommandOperations(deps) {
12239
12249
  throw errors.unprocessable({ details: { reason: outcome.reason } });
12240
12250
  case "not_found":
12241
12251
  throw errors.notFound();
12252
+ case "rate_limited":
12253
+ throw errors.rateLimited();
12242
12254
  }
12243
12255
  }
12244
12256
  function accountActorOf(actor) {
@@ -12828,6 +12840,14 @@ function createCommandOperations(deps) {
12828
12840
  return { value: run.result, replayed: run.replayed };
12829
12841
  });
12830
12842
  },
12843
+ async issueWorkerRegistrationToken(actor) {
12844
+ const subjectId = actor.principal.subjectId;
12845
+ return await audited(actor, { operationId: "worker.issueRegistrationToken", aggregateType: "account", aggregateId: subjectId, projectId: null }, async (policy) => {
12846
+ await require2(actor, "account.manage", { type: "account" }, policy);
12847
+ const issued = unwrap(await write2.issueWorkerRegistrationToken({ owner: accountActorOf(actor) }));
12848
+ return { value: { secret: issued.secret, expiresAt: toTimestamp(issued.expiresAt) }, replayed: false };
12849
+ });
12850
+ },
12831
12851
  async reportInitStatus(actor, command) {
12832
12852
  const { projectId, phase } = command;
12833
12853
  return await audited(actor, { operationId: "project.reportInitStatus", aggregateType: "project", aggregateId: projectId, projectId, data: { phase, failed: command.error !== void 0 } }, async (policy) => {
@@ -13125,8 +13145,8 @@ var init_cutover_preflight = __esm({
13125
13145
  SECRET_CONSUMERS = [
13126
13146
  { consumer: "agent credential in the job bundle (Claude / Codex)", purpose: "agent_execution", implemented: true, note: "issued per job from the project's auth mode" },
13127
13147
  { consumer: "repository clone tokens in the job bundle (GitHub / GitLab / Bitbucket)", purpose: "repository_read", implemented: true, note: "one grant for each host the job clones" },
13128
- { consumer: "Context Repo push of the delivery from the SANDBOX (contextWrite)", purpose: "context_write", implemented: false, note: "open: the delivery has to reach the holder of the materializer role without a write credential on the SANDBOX \u2014 the transport is an owner's decision" },
13129
- { consumer: "mirror clone token in the job bundle (mirrorWrite, read use)", purpose: "repository_read", implemented: false, note: "open: a read grant under the mirror's host, issued with the delivery transport" },
13148
+ { consumer: "Context Repo push of the delivery from the SANDBOX (contextWrite)", purpose: "context_write", implemented: true, note: "the SANDBOX publishes the delivery over the host-local relay and the holder of the materializer role pushes it; no write credential on the SANDBOX" },
13149
+ { consumer: "mirror clone token in the job bundle (mirrorWrite, read use)", purpose: "repository_read", implemented: true, note: "a read grant under the mirror's host, issued by the holder of the role with the other repository grants" },
13130
13150
  { consumer: "tracker writes (YouTrack) \u2014 CLI-side, never on the SANDBOX", purpose: "tracker_write", implemented: true, note: "nothing to move: the holder of the role already writes" },
13131
13151
  { consumer: "named project secrets in custom-action runs \u2014 CLI-side", purpose: "custom_action", implemented: true, note: "nothing to move until a tool-run executes on the SANDBOX" }
13132
13152
  ];
@@ -25805,10 +25825,16 @@ async function deleteSandboxRegistration(db, sandboxId) {
25805
25825
  async function sandboxRoutes(app, deps) {
25806
25826
  const { db, config } = deps;
25807
25827
  const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 30 });
25808
- const regTokenMintByEmail = createAbuseThrottle({ windowMs: 60 * 6e4, max: 30 });
25809
25828
  app.post(
25810
- "/sandbox/register",
25829
+ "/node/v1/sandbox/register",
25811
25830
  {
25831
+ ...routeMeta({
25832
+ surface: "node",
25833
+ operationId: "node.sandbox.register",
25834
+ authPolicy: "public",
25835
+ stability: "preview",
25836
+ summary: "Register a SANDBOX with a registration token"
25837
+ }),
25812
25838
  schema: {
25813
25839
  body: {
25814
25840
  type: "object",
@@ -25849,14 +25875,29 @@ async function sandboxRoutes(app, deps) {
25849
25875
  return { sandboxId: id, token };
25850
25876
  }
25851
25877
  );
25852
- app.get("/sandbox/me", async (req, reply) => {
25878
+ app.get("/node/v1/sandbox/me", routeMeta({
25879
+ surface: "node",
25880
+ operationId: "node.sandbox.me",
25881
+ authPolicy: "node_actor",
25882
+ resourcePolicy: "node.connect",
25883
+ stability: "preview",
25884
+ summary: "Is this SANDBOX registration still known"
25885
+ }), async (req, reply) => {
25853
25886
  const sandbox = await authenticateSandboxByBearer(db, req.headers.authorization);
25854
25887
  if (!sandbox) return reply.status(401).send({ error: "unauthorized" });
25855
25888
  return { sandboxId: sandbox.id, name: sandbox.name };
25856
25889
  });
25857
25890
  app.post(
25858
- "/sandbox/agent-auth",
25891
+ "/node/v1/sandbox/agent-auth",
25859
25892
  {
25893
+ ...routeMeta({
25894
+ surface: "node",
25895
+ operationId: "node.sandbox.agentAuth",
25896
+ authPolicy: "node_actor",
25897
+ resourcePolicy: "node.connect",
25898
+ stability: "preview",
25899
+ summary: "Report the host's agent login and ask whether to probe it"
25900
+ }),
25860
25901
  schema: {
25861
25902
  body: {
25862
25903
  type: "object",
@@ -25892,8 +25933,16 @@ async function sandboxRoutes(app, deps) {
25892
25933
  }
25893
25934
  );
25894
25935
  app.post(
25895
- "/sandbox/agent-auth/result",
25936
+ "/node/v1/sandbox/agent-auth/result",
25896
25937
  {
25938
+ ...routeMeta({
25939
+ surface: "node",
25940
+ operationId: "node.sandbox.agentAuthResult",
25941
+ authPolicy: "node_actor",
25942
+ resourcePolicy: "node.connect",
25943
+ stability: "preview",
25944
+ summary: "Report the verdict of an agent login probe"
25945
+ }),
25897
25946
  schema: {
25898
25947
  body: {
25899
25948
  type: "object",
@@ -25943,19 +25992,6 @@ async function sandboxRoutes(app, deps) {
25943
25992
  app.log.info({ email: session.email, revoked }, "sandbox reg token revoked");
25944
25993
  return { revoked };
25945
25994
  });
25946
- app.post("/sandbox/reg-token/cli", async (req, reply) => {
25947
- const auth = await resolveCliAuth(db, config, req.headers.authorization);
25948
- if (!auth) {
25949
- return reply.status(401).send({ error: "bad_cli_token" });
25950
- }
25951
- if (!regTokenMintByEmail.take(`email:${auth.email}`)) {
25952
- app.log.warn({ email: auth.email }, "ephemeral sandbox reg token mint throttled");
25953
- return reply.status(429).send({ error: "too_many_requests" });
25954
- }
25955
- const { secret, expiresAt } = await issueEphemeralSandboxRegToken(db, auth.email);
25956
- app.log.info({ email: auth.email }, "ephemeral sandbox reg token issued for CLI");
25957
- return { secret, expiresAt };
25958
- });
25959
25995
  app.delete(
25960
25996
  "/sandbox/:sandboxId",
25961
25997
  async (req, reply) => {
@@ -25992,11 +26028,11 @@ var init_sandbox2 = __esm({
25992
26028
  init_sealedbox();
25993
26029
  init_abuse_throttle();
25994
26030
  init_auth();
25995
- init_cli_auth();
25996
26031
  init_sandbox_reg_tokens();
25997
26032
  init_users();
25998
26033
  init_agent_auth_probe();
25999
26034
  init_ws();
26035
+ init_route_registry();
26000
26036
  }
26001
26037
  });
26002
26038
 
@@ -26060,8 +26096,15 @@ async function farmRoutes(app, deps) {
26060
26096
  const { db, config } = deps;
26061
26097
  const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 10 });
26062
26098
  app.post(
26063
- "/farm/register",
26099
+ "/node/v1/farm/register",
26064
26100
  {
26101
+ ...routeMeta({
26102
+ surface: "node",
26103
+ operationId: "node.farm.register",
26104
+ authPolicy: "public",
26105
+ stability: "preview",
26106
+ summary: "Register a FARM with the operator's registration token"
26107
+ }),
26065
26108
  schema: {
26066
26109
  body: {
26067
26110
  type: "object",
@@ -26140,6 +26183,7 @@ var init_farm2 = __esm({
26140
26183
  init_sealedbox();
26141
26184
  init_abuse_throttle();
26142
26185
  init_ws();
26186
+ init_route_registry();
26143
26187
  }
26144
26188
  });
26145
26189
 
@@ -26241,8 +26285,15 @@ async function runnerRoutes(app, deps) {
26241
26285
  const { db, config } = deps;
26242
26286
  const registerByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 10 });
26243
26287
  app.post(
26244
- "/runner/register",
26288
+ "/node/v1/runner/register",
26245
26289
  {
26290
+ ...routeMeta({
26291
+ surface: "node",
26292
+ operationId: "node.runner.register",
26293
+ authPolicy: "public",
26294
+ stability: "preview",
26295
+ summary: "Register a RUNNER host with a personal registration token"
26296
+ }),
26246
26297
  schema: {
26247
26298
  body: {
26248
26299
  type: "object",
@@ -26345,6 +26396,7 @@ var init_runner2 = __esm({
26345
26396
  init_users();
26346
26397
  init_runner_reg_tokens();
26347
26398
  init_ws();
26399
+ init_route_registry();
26348
26400
  }
26349
26401
  });
26350
26402
 
@@ -27524,23 +27576,11 @@ function installSocketKeepalive(socket, periodMs) {
27524
27576
  }
27525
27577
  async function wsRoutes(app, deps) {
27526
27578
  const { db, config } = deps;
27527
- async function authenticateSocket(req, component, resolve4) {
27528
- for (const secret of readNodeSocketSecrets(req.headers, req.query)) {
27529
- const resolved = await resolve4(secret.token);
27530
- if (resolved === null || resolved === void 0) continue;
27531
- if (secret.source === "query") warnAboutQueryCredential(component, req.ip ?? null);
27532
- return resolved;
27533
- }
27534
- return null;
27535
- }
27536
- const warnedComponents = /* @__PURE__ */ new Set();
27537
- function warnAboutQueryCredential(component, remote) {
27538
- if (warnedComponents.has(component)) return;
27539
- warnedComponents.add(component);
27540
- app.log.warn(
27541
- { component, remote },
27542
- "node socket authenticated from the query string \u2014 the credential lands in proxy logs; update the component to send it in the Authorization header (this is logged once)"
27543
- );
27579
+ async function authenticateSocket(req, resolve4) {
27580
+ const token = readNodeSocketSecret(req.headers);
27581
+ if (token === null) return null;
27582
+ const resolved = await resolve4(token);
27583
+ return resolved === null || resolved === void 0 ? null : resolved;
27544
27584
  }
27545
27585
  const cliSocket = async (socket, req) => {
27546
27586
  let boundProjectId = null;
@@ -27877,7 +27917,7 @@ async function wsRoutes(app, deps) {
27877
27917
  app.log.info({ remote: req.ip }, "CLI socket closed (never registered)");
27878
27918
  }
27879
27919
  });
27880
- auth = await authenticateSocket(req, "cli", (token) => authFromSecret(db, config, token));
27920
+ auth = await authenticateSocket(req, (token) => authFromSecret(db, config, token));
27881
27921
  if (!auth) {
27882
27922
  socket.close(1008, "unauthorized");
27883
27923
  return;
@@ -27895,7 +27935,7 @@ async function wsRoutes(app, deps) {
27895
27935
  void pump();
27896
27936
  };
27897
27937
  const sandboxSocket = async (socket, req) => {
27898
- const sandbox = await authenticateSocket(req, "sandbox", (token) => findSandboxByToken(db, token));
27938
+ const sandbox = await authenticateSocket(req, (token) => findSandboxByToken(db, token));
27899
27939
  if (!sandbox) {
27900
27940
  socket.close(1008, "unauthorized");
27901
27941
  return;
@@ -28013,7 +28053,7 @@ async function wsRoutes(app, deps) {
28013
28053
  });
28014
28054
  };
28015
28055
  const farmSocket = async (socket, req) => {
28016
- const farm = await authenticateSocket(req, "farm", (token) => findFarmByToken(db, token));
28056
+ const farm = await authenticateSocket(req, (token) => findFarmByToken(db, token));
28017
28057
  if (!farm) {
28018
28058
  socket.close(1008, "unauthorized");
28019
28059
  return;
@@ -28105,7 +28145,7 @@ async function wsRoutes(app, deps) {
28105
28145
  });
28106
28146
  };
28107
28147
  const runnerSocket = async (socket, req) => {
28108
- const runner = await authenticateSocket(req, "runner", (token) => findRunnerByToken(db, token));
28148
+ const runner = await authenticateSocket(req, (token) => findRunnerByToken(db, token));
28109
28149
  if (!runner) {
28110
28150
  socket.close(1008, "unauthorized");
28111
28151
  return;
@@ -28237,10 +28277,12 @@ async function wsRoutes(app, deps) {
28237
28277
  }
28238
28278
  const auth = parseNodeAuth(parsed);
28239
28279
  if (auth !== null) {
28240
- if (auth.component !== component || !await authenticate({ ...req, headers: { authorization: `Bearer ${auth.token}` } })) {
28280
+ const withFrameCredential = { ...req, headers: { ...req.headers, authorization: `Bearer ${auth.token}` } };
28281
+ if (auth.component !== component || !await authenticate(withFrameCredential)) {
28241
28282
  refuse("unauthenticated");
28242
28283
  return;
28243
28284
  }
28285
+ effectiveReq = withFrameCredential;
28244
28286
  authenticated = true;
28245
28287
  return;
28246
28288
  }
@@ -28277,7 +28319,7 @@ async function wsRoutes(app, deps) {
28277
28319
  socket.off("message", listener);
28278
28320
  socket.pause();
28279
28321
  try {
28280
- await inner(socket, req);
28322
+ await inner(socket, effectiveReq);
28281
28323
  } catch (err) {
28282
28324
  app.log.error({ err, component }, "node socket handler failed after handshake");
28283
28325
  socket.close(1011, "handler_failed");
@@ -28288,6 +28330,7 @@ async function wsRoutes(app, deps) {
28288
28330
  socket.resume();
28289
28331
  };
28290
28332
  let authenticated = false;
28333
+ let effectiveReq = req;
28291
28334
  const ready = authenticate(req).then((ok2) => {
28292
28335
  authenticated = ok2;
28293
28336
  });
@@ -28321,7 +28364,6 @@ async function wsRoutes(app, deps) {
28321
28364
  });
28322
28365
  }
28323
28366
  }
28324
- app.get("/cli/connect", { websocket: true }, cliSocket);
28325
28367
  app.get(
28326
28368
  "/node/v1/cli/connect",
28327
28369
  {
@@ -28335,9 +28377,8 @@ async function wsRoutes(app, deps) {
28335
28377
  summary: "Control socket of a CLI instance"
28336
28378
  })
28337
28379
  },
28338
- withNodeHandshake("cli", async (r) => await authenticateSocket(r, "cli", (token) => authFromSecret(db, config, token)) !== null, cliSocket)
28380
+ withNodeHandshake("cli", async (r) => await authenticateSocket(r, (token) => authFromSecret(db, config, token)) !== null, cliSocket)
28339
28381
  );
28340
- app.get("/sandbox/connect", { websocket: true }, sandboxSocket);
28341
28382
  app.get(
28342
28383
  "/node/v1/sandbox/connect",
28343
28384
  {
@@ -28351,9 +28392,8 @@ async function wsRoutes(app, deps) {
28351
28392
  summary: "Control socket of a SANDBOX instance"
28352
28393
  })
28353
28394
  },
28354
- withNodeHandshake("sandbox", async (r) => await authenticateSocket(r, "sandbox", (token) => findSandboxByToken(db, token)) !== null, sandboxSocket)
28395
+ withNodeHandshake("sandbox", async (r) => await authenticateSocket(r, (token) => findSandboxByToken(db, token)) !== null, sandboxSocket)
28355
28396
  );
28356
- app.get("/farm/connect", { websocket: true }, farmSocket);
28357
28397
  app.get(
28358
28398
  "/node/v1/farm/connect",
28359
28399
  {
@@ -28367,9 +28407,8 @@ async function wsRoutes(app, deps) {
28367
28407
  summary: "Control socket of a FARM instance"
28368
28408
  })
28369
28409
  },
28370
- withNodeHandshake("farm", async (r) => await authenticateSocket(r, "farm", (token) => findFarmByToken(db, token)) !== null, farmSocket)
28410
+ withNodeHandshake("farm", async (r) => await authenticateSocket(r, (token) => findFarmByToken(db, token)) !== null, farmSocket)
28371
28411
  );
28372
- app.get("/runner/connect", { websocket: true }, runnerSocket);
28373
28412
  app.get(
28374
28413
  "/node/v1/runner/connect",
28375
28414
  {
@@ -28383,7 +28422,7 @@ async function wsRoutes(app, deps) {
28383
28422
  summary: "Control socket of a RUNNER instance"
28384
28423
  })
28385
28424
  },
28386
- withNodeHandshake("runner", async (r) => await authenticateSocket(r, "runner", (token) => findRunnerByToken(db, token)) !== null, runnerSocket)
28425
+ withNodeHandshake("runner", async (r) => await authenticateSocket(r, (token) => findRunnerByToken(db, token)) !== null, runnerSocket)
28387
28426
  );
28388
28427
  }
28389
28428
  var agentLoginProviders, deliveryFollowers, projectCliConnections, projectLastSeenAt, projectLastDuplicateRegisterAt, sandboxConnections, farmConnections, runnerConnections, cliMessageHandlers, cliRegisterHandlers, cliDisconnectHandlers, farmMessageHandlers, runnerMessageHandlers, providerMessageHandlers, RECOGNIZED_NODE_CAPABILITIES, NODE_HANDSHAKE_BACKLOG, NODE_HANDSHAKE_TIMEOUT_MS;
@@ -28879,9 +28918,7 @@ var DASHBOARD_ROUTES = [
28879
28918
  var CONSOLE_ROUTES = [/^\/$/];
28880
28919
  var DECLARED_RESERVED_ROOTS = [
28881
28920
  ...Object.values(SURFACE_PREFIXES).flat(),
28882
- ...RETIRED_ROOTS,
28883
- "/cli",
28884
- "/ws"
28921
+ ...RETIRED_ROOTS
28885
28922
  ];
28886
28923
  function isBrowserRoute(app, path) {
28887
28924
  const clean = path.split("?")[0].split("#")[0];
@@ -28978,9 +29015,22 @@ var RETIRED_SOCKET_PATHS = [
28978
29015
  "/farm/connect",
28979
29016
  "/runner/connect"
28980
29017
  ];
29018
+ var RETIRED_NODE_PATHS = [
29019
+ "/jobs/claim",
29020
+ "/jobs/heartbeat",
29021
+ "/jobs/release",
29022
+ "/jobs/result",
29023
+ "/sandbox/register",
29024
+ "/sandbox/me",
29025
+ "/sandbox/agent-auth",
29026
+ "/sandbox/agent-auth/result",
29027
+ "/sandbox/reg-token/cli",
29028
+ "/farm/register",
29029
+ "/runner/register"
29030
+ ];
28981
29031
  function isRetiredClientPath(url, registeredPaths = []) {
28982
29032
  const path = url.split("?")[0].split("#")[0];
28983
- if (RETIRED_SOCKET_PATHS.includes(path)) return !registeredPaths.includes(path);
29033
+ if (RETIRED_SOCKET_PATHS.includes(path) || RETIRED_NODE_PATHS.includes(path)) return !registeredPaths.includes(path);
28984
29034
  const retired = RETIRED_ROOTS.find((root) => path === root || path.startsWith(`${root}/`));
28985
29035
  if (retired === void 0) return false;
28986
29036
  return !registeredPaths.some((p) => p === retired || p.startsWith(`${retired}/`));
@@ -29032,6 +29082,121 @@ async function registerSpaHosts(app, opts) {
29032
29082
  // src/server.ts
29033
29083
  init_route_registry();
29034
29084
 
29085
+ // src/http/legacy-routes.ts
29086
+ var STATIC_PLUGIN_ROUTES = ["GET /*"];
29087
+ var LEGACY_ROUTES = [
29088
+ "DELETE /api/admin/farms/:farmId",
29089
+ "DELETE /api/admin/runners/:runnerId",
29090
+ "DELETE /api/admin/sandboxes/:sandboxId",
29091
+ "DELETE /projects/:projectId/secrets/:name",
29092
+ "DELETE /runner/:runnerId",
29093
+ "DELETE /runner/reg-token",
29094
+ "DELETE /sandbox/:sandboxId",
29095
+ "DELETE /sandbox/reg-token",
29096
+ "GET /admin/pause",
29097
+ "GET /api/admin/farms",
29098
+ "GET /api/admin/logs",
29099
+ "GET /api/admin/presence",
29100
+ "GET /api/admin/projects",
29101
+ "GET /api/admin/registrations",
29102
+ "GET /api/admin/runners",
29103
+ "GET /api/admin/server",
29104
+ "GET /api/admin/settings",
29105
+ "GET /api/admin/stats",
29106
+ "GET /api/admin/users",
29107
+ "GET /api/notifications/settings",
29108
+ "GET /api/projects/:projectId/tasks/:taskKey/plan-review",
29109
+ "GET /bug-report",
29110
+ "GET /projects/:projectId/actions",
29111
+ "GET /projects/:projectId/adr-backfill",
29112
+ "GET /projects/:projectId/clusters/:anchorKey/journal",
29113
+ "GET /projects/:projectId/config-proposals/:proposalId/agent-activity/stream",
29114
+ "GET /projects/:projectId/context-file",
29115
+ "GET /projects/:projectId/drafts/:draftId/agent-activity/stream",
29116
+ "GET /projects/:projectId/model/drift",
29117
+ "GET /projects/:projectId/model/search",
29118
+ "GET /projects/:projectId/pause",
29119
+ "GET /projects/:projectId/prompts",
29120
+ "GET /projects/:projectId/prompts/:role",
29121
+ "GET /projects/:projectId/review-metrics",
29122
+ "GET /projects/:projectId/runs",
29123
+ "GET /projects/:projectId/runs/:runId/stream",
29124
+ "GET /projects/:projectId/tasks/:taskKey/actions",
29125
+ "GET /projects/:projectId/tasks/:taskKey/agent-activity/stream",
29126
+ "GET /projects/:projectId/tasks/:taskKey/external-review/draft",
29127
+ "GET /projects/:projectId/tasks/:taskKey/review-rounds",
29128
+ "GET /projects/:projectId/tracker-status",
29129
+ "GET /projects/:projectId/tracker/fields",
29130
+ "GET /runner/reg-token",
29131
+ "GET /runners",
29132
+ "GET /sandbox/reg-token",
29133
+ "POST /admin/pause",
29134
+ "POST /api/admin/farms/:farmId/assign",
29135
+ "POST /api/admin/farms/:farmId/unassign",
29136
+ "POST /api/admin/registrations/:id/approve",
29137
+ "POST /api/admin/registrations/:id/reject",
29138
+ "POST /api/admin/runners/:runnerId/owner",
29139
+ "POST /api/admin/users",
29140
+ "POST /api/admin/users/:email/activate",
29141
+ "POST /api/admin/users/:email/block",
29142
+ "POST /api/projects/:projectId/tasks/:taskKey/develop",
29143
+ "POST /api/projects/:projectId/tasks/:taskKey/observer-answer",
29144
+ "POST /api/projects/:projectId/tasks/:taskKey/plan-review",
29145
+ "POST /api/projects/:projectId/tasks/:taskKey/stage",
29146
+ "POST /api/projects/:projectId/tasks/:taskKey/workflow-route",
29147
+ "POST /bug-report",
29148
+ "POST /projects/:projectId/actions/:actionId/run",
29149
+ "POST /projects/:projectId/adr-backfill/discard",
29150
+ "POST /projects/:projectId/adr-backfill/promote",
29151
+ "POST /projects/:projectId/check-readiness",
29152
+ "POST /projects/:projectId/clusters/:anchorKey/reject",
29153
+ "POST /projects/:projectId/creds",
29154
+ "POST /projects/:projectId/detach-from-farm",
29155
+ "POST /projects/:projectId/gh-runner/connect",
29156
+ "POST /projects/:projectId/gh-runner/disconnect",
29157
+ "POST /projects/:projectId/model-bootstrap",
29158
+ "POST /projects/:projectId/move-to-farm",
29159
+ "POST /projects/:projectId/pause",
29160
+ "POST /projects/:projectId/refresh-context-status",
29161
+ "POST /projects/:projectId/retry-init",
29162
+ "POST /projects/:projectId/secrets",
29163
+ "POST /projects/:projectId/sync-config",
29164
+ "POST /projects/:projectId/sync-context",
29165
+ "POST /projects/:projectId/tasks/:taskKey/actions/:actionId/run",
29166
+ "POST /projects/:projectId/tasks/:taskKey/approve",
29167
+ "POST /projects/:projectId/tasks/:taskKey/build-flag",
29168
+ "POST /projects/:projectId/tasks/:taskKey/cancel",
29169
+ "POST /projects/:projectId/tasks/:taskKey/comment",
29170
+ "POST /projects/:projectId/tasks/:taskKey/external-review/close",
29171
+ "POST /projects/:projectId/tasks/:taskKey/external-review/dismiss",
29172
+ "POST /projects/:projectId/tasks/:taskKey/external-review/publish",
29173
+ "POST /projects/:projectId/tasks/:taskKey/external-review/rerun",
29174
+ "POST /projects/:projectId/tasks/:taskKey/external-review/start",
29175
+ "POST /projects/:projectId/tasks/:taskKey/force-done",
29176
+ "POST /projects/:projectId/tasks/:taskKey/open-pr",
29177
+ "POST /projects/:projectId/tasks/:taskKey/pause",
29178
+ "POST /projects/:projectId/tasks/:taskKey/publish-branch",
29179
+ "POST /projects/:projectId/tasks/:taskKey/reject",
29180
+ "POST /projects/:projectId/tasks/:taskKey/restart",
29181
+ "POST /projects/:projectId/tasks/:taskKey/retry",
29182
+ "POST /projects/:projectId/tasks/:taskKey/to-backlog",
29183
+ "POST /projects/:projectId/tasks/:taskKey/tracker-status",
29184
+ "POST /projects/:projectId/tasks/:taskKey/unarchive",
29185
+ "POST /projects/:projectId/tracker-sync",
29186
+ "POST /runner/reg-token",
29187
+ "POST /sandbox/reg-token",
29188
+ "PUT /api/admin/farms/:farmId/quota",
29189
+ "PUT /api/admin/settings",
29190
+ "PUT /api/notifications/settings",
29191
+ "PUT /projects/:projectId/adr/:file",
29192
+ "PUT /projects/:projectId/prompts/:role",
29193
+ "PUT /projects/:projectId/tasks/:taskKey/branch-name",
29194
+ "PUT /projects/:projectId/tasks/:taskKey/cluster-base",
29195
+ "PUT /projects/:projectId/tasks/:taskKey/commit-summary",
29196
+ "PUT /projects/:projectId/tasks/:taskKey/description",
29197
+ "PUT /projects/:projectId/tasks/:taskKey/solution"
29198
+ ];
29199
+
29035
29200
  // src/http/browser-security.ts
29036
29201
  init_auth();
29037
29202
  import { createHash as createHash9, randomBytes as randomBytes3 } from "node:crypto";
@@ -31024,6 +31189,28 @@ async function apiV1MutationRoutes(app, deps) {
31024
31189
  }
31025
31190
  }
31026
31191
  );
31192
+ app.post(
31193
+ "/workers/registration-token",
31194
+ routeMeta({
31195
+ surface: "application",
31196
+ operationId: "worker.issueRegistrationToken",
31197
+ authPolicy: "application_actor",
31198
+ resourcePolicy: "account.manage",
31199
+ stability: "preview",
31200
+ summary: "Mint a single-use registration token for a worker of this account"
31201
+ }),
31202
+ async (req, reply) => {
31203
+ const requestId = requestIdOf(req);
31204
+ try {
31205
+ const actor = await actorOr401({ db, config }, req, reply, requestId);
31206
+ if (!actor) return reply;
31207
+ const issued = await commands.issueWorkerRegistrationToken(actor);
31208
+ return reply.status(201).send(issued);
31209
+ } catch (err) {
31210
+ return sendError(reply, err, requestId);
31211
+ }
31212
+ }
31213
+ );
31027
31214
  app.post(
31028
31215
  "/projects/:projectId/init-status",
31029
31216
  routeMeta({
@@ -34145,6 +34332,8 @@ function requestGhRunnersDelete(runnerId, projectId, sealedBundle, timeoutMs) {
34145
34332
  init_farm2();
34146
34333
  init_runner2();
34147
34334
  init_init_status();
34335
+ init_sandbox_reg_tokens();
34336
+ init_abuse_throttle();
34148
34337
 
34149
34338
  // src/projects/forget.ts
34150
34339
  init_legacy_cred_bundles();
@@ -34426,6 +34615,7 @@ function nodeUnavailable(capability, err) {
34426
34615
  function taskState(row) {
34427
34616
  return { ...row, revision: String(row.updatedAt) };
34428
34617
  }
34618
+ var WORKER_REG_TOKEN_MINTS = createAbuseThrottle({ windowMs: 60 * 6e4, max: 30 });
34429
34619
  function createWriteModel(db, log, hooks = {}) {
34430
34620
  async function readTaskRow(projectId, key) {
34431
34621
  const row = await db.get(
@@ -35108,6 +35298,16 @@ function createWriteModel(db, log, hooks = {}) {
35108
35298
  log.info({ projectId, jobId, kind }, kind === "backfill" ? "backfill job enqueued" : "index job enqueued");
35109
35299
  return ok({ jobId });
35110
35300
  },
35301
+ async issueWorkerRegistrationToken({ owner }) {
35302
+ if (owner.email === null) return { ok: false, refusal: "unprocessable", reason: "account_has_no_email" };
35303
+ if (!WORKER_REG_TOKEN_MINTS.take(`email:${owner.email}`)) {
35304
+ log.warn({ email: owner.email }, "ephemeral sandbox reg token mint throttled");
35305
+ return { ok: false, refusal: "rate_limited" };
35306
+ }
35307
+ const issued = await issueEphemeralSandboxRegToken(db, owner.email);
35308
+ log.info({ email: owner.email }, "ephemeral sandbox reg token issued for a client");
35309
+ return ok(issued);
35310
+ },
35111
35311
  async reportInitStatus({ projectId, phase, error }) {
35112
35312
  const recorded = await advanceInitPhase(db, projectId, phase);
35113
35313
  if (error !== null) {
@@ -37925,6 +38125,7 @@ function clearPasswordAttempts(key) {
37925
38125
 
37926
38126
  // src/routes/auth.ts
37927
38127
  init_users();
38128
+ init_route_registry();
37928
38129
  async function authRoutes(app, deps) {
37929
38130
  const { db, config, mailer } = deps;
37930
38131
  const loginByIp = createAbuseThrottle({ windowMs: 15 * 6e4, max: 30 });
@@ -37965,18 +38166,21 @@ async function authRoutes(app, deps) {
37965
38166
  function registerAccountRoutes() {
37966
38167
  app.post(
37967
38168
  "/auth/login",
37968
- {
37969
- schema: {
37970
- body: {
37971
- type: "object",
37972
- required: ["email"],
37973
- properties: {
37974
- email: { type: "string", minLength: 3, maxLength: 254 },
37975
- pwa_nonce: { type: "string", minLength: 1, maxLength: 64 }
37976
- }
38169
+ routeMeta({
38170
+ surface: "bootstrap",
38171
+ operationId: "auth.login",
38172
+ authPolicy: "public",
38173
+ stability: "preview",
38174
+ summary: "Sign in: send a magic link",
38175
+ requestSchema: {
38176
+ type: "object",
38177
+ required: ["email"],
38178
+ properties: {
38179
+ email: { type: "string", minLength: 3, maxLength: 254 },
38180
+ pwa_nonce: { type: "string", minLength: 1, maxLength: 64 }
37977
38181
  }
37978
38182
  }
37979
- },
38183
+ }),
37980
38184
  async (req, reply) => {
37981
38185
  const email = (req.body.email ?? "").trim().toLowerCase();
37982
38186
  if (loginThrottled(email, req.ip)) {
@@ -37994,18 +38198,21 @@ async function authRoutes(app, deps) {
37994
38198
  );
37995
38199
  app.post(
37996
38200
  "/auth/login-start",
37997
- {
37998
- schema: {
37999
- body: {
38000
- type: "object",
38001
- required: ["email"],
38002
- properties: {
38003
- email: { type: "string", minLength: 3, maxLength: 254 },
38004
- pwa_nonce: { type: "string", minLength: 1, maxLength: 64 }
38005
- }
38201
+ routeMeta({
38202
+ surface: "bootstrap",
38203
+ operationId: "auth.loginStart",
38204
+ authPolicy: "public",
38205
+ stability: "preview",
38206
+ summary: "Start a sign-in, without saying whether the account exists",
38207
+ requestSchema: {
38208
+ type: "object",
38209
+ required: ["email"],
38210
+ properties: {
38211
+ email: { type: "string", minLength: 3, maxLength: 254 },
38212
+ pwa_nonce: { type: "string", minLength: 1, maxLength: 64 }
38006
38213
  }
38007
38214
  }
38008
- },
38215
+ }),
38009
38216
  async (req, reply) => {
38010
38217
  const email = (req.body.email ?? "").trim().toLowerCase();
38011
38218
  if (loginThrottled(email, req.ip)) {
@@ -38027,18 +38234,21 @@ async function authRoutes(app, deps) {
38027
38234
  );
38028
38235
  app.post(
38029
38236
  "/auth/login-password",
38030
- {
38031
- schema: {
38032
- body: {
38033
- type: "object",
38034
- required: ["email", "password"],
38035
- properties: {
38036
- email: { type: "string", minLength: 3, maxLength: 254 },
38037
- password: { type: "string", minLength: 1, maxLength: MAX_PASSWORD_LENGTH }
38038
- }
38237
+ routeMeta({
38238
+ surface: "bootstrap",
38239
+ operationId: "auth.loginPassword",
38240
+ authPolicy: "public",
38241
+ stability: "preview",
38242
+ summary: "Sign in with a password",
38243
+ requestSchema: {
38244
+ type: "object",
38245
+ required: ["email", "password"],
38246
+ properties: {
38247
+ email: { type: "string", minLength: 3, maxLength: 254 },
38248
+ password: { type: "string", minLength: 1, maxLength: MAX_PASSWORD_LENGTH }
38039
38249
  }
38040
38250
  }
38041
- },
38251
+ }),
38042
38252
  async (req, reply) => {
38043
38253
  const email = (req.body.email ?? "").trim().toLowerCase();
38044
38254
  const password = req.body.password ?? "";
@@ -38062,15 +38272,18 @@ async function authRoutes(app, deps) {
38062
38272
  );
38063
38273
  app.post(
38064
38274
  "/auth/set-password",
38065
- {
38066
- schema: {
38067
- body: {
38068
- type: "object",
38069
- required: ["password"],
38070
- properties: { password: { type: "string", minLength: 1, maxLength: 512 } }
38071
- }
38275
+ routeMeta({
38276
+ surface: "bootstrap",
38277
+ operationId: "auth.setPassword",
38278
+ authPolicy: "session",
38279
+ stability: "preview",
38280
+ summary: "Set the signed-in account's password",
38281
+ requestSchema: {
38282
+ type: "object",
38283
+ required: ["password"],
38284
+ properties: { password: { type: "string", minLength: 1, maxLength: 512 } }
38072
38285
  }
38073
- },
38286
+ }),
38074
38287
  async (req, reply) => {
38075
38288
  const session = await requireSession(db, req, reply);
38076
38289
  if (!session) return reply;
@@ -38089,7 +38302,14 @@ async function authRoutes(app, deps) {
38089
38302
  return reply.send({ status: "ok" });
38090
38303
  }
38091
38304
  );
38092
- app.get("/auth/verify", async (req, reply) => {
38305
+ app.get("/auth/verify", routeMeta({
38306
+ surface: "bootstrap",
38307
+ operationId: "auth.verify",
38308
+ authPolicy: "public",
38309
+ // A redirect from an email link, not a JSON operation to generate a client for.
38310
+ stability: "internal",
38311
+ summary: "Redeem a magic link"
38312
+ }), async (req, reply) => {
38093
38313
  const wantsHtml = (req.headers.accept ?? "").toLowerCase().includes("text/html");
38094
38314
  const token = (req.query.token ?? "").trim();
38095
38315
  if (!token) {
@@ -38111,7 +38331,14 @@ async function authRoutes(app, deps) {
38111
38331
  });
38112
38332
  }
38113
38333
  function registerSessionRoutes() {
38114
- app.get("/auth/me", async (req, reply) => {
38334
+ app.get("/auth/me", routeMeta({
38335
+ surface: "bootstrap",
38336
+ operationId: "auth.me",
38337
+ // A cookie, or the single-owner's credential on the local profile.
38338
+ authPolicy: "application_actor",
38339
+ stability: "preview",
38340
+ summary: "Who is signed in"
38341
+ }), async (req, reply) => {
38115
38342
  const session = await readSession(db, req);
38116
38343
  if (!session) return reply.status(401).send({ error: "no_session" });
38117
38344
  const user = await findUser(db, session.email);
@@ -38123,7 +38350,13 @@ async function authRoutes(app, deps) {
38123
38350
  singleOwner
38124
38351
  };
38125
38352
  });
38126
- app.post("/auth/logout", async (req, reply) => {
38353
+ app.post("/auth/logout", routeMeta({
38354
+ surface: "bootstrap",
38355
+ operationId: "auth.logout",
38356
+ authPolicy: "public",
38357
+ stability: "preview",
38358
+ summary: "Sign out"
38359
+ }), async (req, reply) => {
38127
38360
  const cookie2 = req.cookies[SESSION_COOKIE_NAME];
38128
38361
  if (cookie2) {
38129
38362
  const unsigned = req.unsignCookie(cookie2);
@@ -38134,7 +38367,13 @@ async function authRoutes(app, deps) {
38134
38367
  reply.clearCookie(SESSION_COOKIE_NAME, { path: "/" });
38135
38368
  return { status: "ok" };
38136
38369
  });
38137
- app.get("/auth/pwa-ready", async (req, reply) => {
38370
+ app.get("/auth/pwa-ready", routeMeta({
38371
+ surface: "bootstrap",
38372
+ operationId: "auth.pwaReady",
38373
+ authPolicy: "public",
38374
+ stability: "preview",
38375
+ summary: "Whether an installed app's sign-in has completed"
38376
+ }), async (req, reply) => {
38138
38377
  const nonce = (req.query.nonce ?? "").trim();
38139
38378
  if (!nonce) return reply.status(400).send({ error: "missing_nonce" });
38140
38379
  const result = await consumePwaNonce(db, nonce);
@@ -38159,6 +38398,7 @@ import { nanoid as nanoid41 } from "nanoid";
38159
38398
  init_settings();
38160
38399
  init_users();
38161
38400
  init_cli_tokens();
38401
+ init_route_registry();
38162
38402
  var REQUEST_TTL_MS = 15 * 6e4;
38163
38403
  var APPROVAL_TTL_MS = 24 * 60 * 6e4;
38164
38404
  async function cliBootstrapRoutes(app, deps) {
@@ -38192,15 +38432,18 @@ async function cliBootstrapRoutes(app, deps) {
38192
38432
  }
38193
38433
  app.post(
38194
38434
  "/cli/bootstrap-request",
38195
- {
38196
- schema: {
38197
- body: {
38198
- type: "object",
38199
- required: ["email"],
38200
- properties: { email: { type: "string", minLength: 3, maxLength: 254 } }
38201
- }
38435
+ routeMeta({
38436
+ surface: "bootstrap",
38437
+ operationId: "bootstrap.request",
38438
+ authPolicy: "public",
38439
+ stability: "preview",
38440
+ summary: "A CLI asks to be connected to an account",
38441
+ requestSchema: {
38442
+ type: "object",
38443
+ required: ["email"],
38444
+ properties: { email: { type: "string", minLength: 3, maxLength: 254 } }
38202
38445
  }
38203
- },
38446
+ }),
38204
38447
  async (req, reply) => {
38205
38448
  const email = (req.body.email ?? "").trim().toLowerCase();
38206
38449
  if (!isValidEmail(email)) {
@@ -38248,6 +38491,14 @@ async function cliBootstrapRoutes(app, deps) {
38248
38491
  );
38249
38492
  app.get(
38250
38493
  "/cli/authorize",
38494
+ routeMeta({
38495
+ surface: "bootstrap",
38496
+ operationId: "bootstrap.authorizePage",
38497
+ authPolicy: "public",
38498
+ // An HTML page, not a JSON operation to generate a client for.
38499
+ stability: "internal",
38500
+ summary: "The page where a person confirms a CLI connection"
38501
+ }),
38251
38502
  async (req, reply) => {
38252
38503
  const requestId = (req.query.request ?? "").trim();
38253
38504
  const row = requestId ? await loadRequest(requestId) : void 0;
@@ -38296,15 +38547,19 @@ async function cliBootstrapRoutes(app, deps) {
38296
38547
  );
38297
38548
  app.post(
38298
38549
  "/cli/authorize/confirm",
38299
- {
38300
- schema: {
38301
- body: {
38302
- type: "object",
38303
- required: ["request"],
38304
- properties: { request: { type: "string", minLength: 1, maxLength: 64 } }
38305
- }
38550
+ routeMeta({
38551
+ surface: "bootstrap",
38552
+ operationId: "bootstrap.authorizeConfirm",
38553
+ // A cookie, or the single-owner's credential on the local profile.
38554
+ authPolicy: "application_actor",
38555
+ stability: "preview",
38556
+ summary: "Confirm a CLI connection as the signed-in person",
38557
+ requestSchema: {
38558
+ type: "object",
38559
+ required: ["request"],
38560
+ properties: { request: { type: "string", minLength: 1, maxLength: 64 } }
38306
38561
  }
38307
- },
38562
+ }),
38308
38563
  async (req, reply) => {
38309
38564
  const session = await readSession(db, req);
38310
38565
  if (!session || !await isAllowedLogin(db, config, session.email)) {
@@ -38322,15 +38577,18 @@ async function cliBootstrapRoutes(app, deps) {
38322
38577
  );
38323
38578
  app.post(
38324
38579
  "/cli/authorize/email",
38325
- {
38326
- schema: {
38327
- body: {
38328
- type: "object",
38329
- required: ["request"],
38330
- properties: { request: { type: "string", minLength: 1, maxLength: 64 } }
38331
- }
38580
+ routeMeta({
38581
+ surface: "bootstrap",
38582
+ operationId: "bootstrap.authorizeEmail",
38583
+ authPolicy: "public",
38584
+ stability: "preview",
38585
+ summary: "Confirm a CLI connection by email instead",
38586
+ requestSchema: {
38587
+ type: "object",
38588
+ required: ["request"],
38589
+ properties: { request: { type: "string", minLength: 1, maxLength: 64 } }
38332
38590
  }
38333
- },
38591
+ }),
38334
38592
  async (req, reply) => {
38335
38593
  const requestId = (req.body.request ?? "").trim();
38336
38594
  const row = await loadRequest(requestId);
@@ -38348,6 +38606,13 @@ async function cliBootstrapRoutes(app, deps) {
38348
38606
  );
38349
38607
  app.get(
38350
38608
  "/cli/bootstrap-confirm",
38609
+ routeMeta({
38610
+ surface: "bootstrap",
38611
+ operationId: "bootstrap.confirm",
38612
+ authPolicy: "public",
38613
+ stability: "internal",
38614
+ summary: "Redeem the CLI connection link from the email"
38615
+ }),
38351
38616
  async (req, reply) => {
38352
38617
  const token = (req.query.token ?? "").trim();
38353
38618
  reply.type("text/html");
@@ -38398,6 +38663,13 @@ async function cliBootstrapRoutes(app, deps) {
38398
38663
  );
38399
38664
  app.get(
38400
38665
  "/cli/bootstrap-poll",
38666
+ routeMeta({
38667
+ surface: "bootstrap",
38668
+ operationId: "bootstrap.poll",
38669
+ authPolicy: "public",
38670
+ stability: "preview",
38671
+ summary: "A CLI polls whether its connection was confirmed"
38672
+ }),
38401
38673
  async (req, reply) => {
38402
38674
  const requestId = (req.query.request ?? "").trim();
38403
38675
  if (!requestId) return reply.status(400).send({ error: "missing_request" });
@@ -38437,13 +38709,20 @@ import { nanoid as nanoid42 } from "nanoid";
38437
38709
  init_auth();
38438
38710
  init_settings();
38439
38711
  init_users();
38712
+ init_route_registry();
38440
38713
  var CHALLENGE_TTL_MS = 10 * 6e4;
38441
38714
  var MIN_FILL_MS = 2e3;
38442
38715
  async function registrationRoutes(app, deps) {
38443
38716
  const { db, config, mailer } = deps;
38444
38717
  const submitByIp = createAbuseThrottle({ windowMs: 60 * 6e4, max: 10 });
38445
38718
  const challengeByIp = createAbuseThrottle({ windowMs: 60 * 6e4, max: 60 });
38446
- app.get("/auth/register-challenge", async (req, reply) => {
38719
+ app.get("/auth/register-challenge", routeMeta({
38720
+ surface: "bootstrap",
38721
+ operationId: "auth.registerChallenge",
38722
+ authPolicy: "public",
38723
+ stability: "preview",
38724
+ summary: "The challenge a registration form answers"
38725
+ }), async (req, reply) => {
38447
38726
  if (!challengeByIp.take(`ip:${req.ip}`)) {
38448
38727
  return reply.status(429).send({ error: "too_many_requests" });
38449
38728
  }
@@ -38457,21 +38736,24 @@ async function registrationRoutes(app, deps) {
38457
38736
  });
38458
38737
  app.post(
38459
38738
  "/auth/register",
38460
- {
38461
- schema: {
38462
- body: {
38463
- type: "object",
38464
- required: ["email", "challengeId"],
38465
- properties: {
38466
- email: { type: "string", minLength: 3, maxLength: 254 },
38467
- name: { type: "string", maxLength: 200 },
38468
- note: { type: "string", maxLength: 1e3 },
38469
- challengeId: { type: "string", minLength: 1, maxLength: 64 },
38470
- website: { type: "string", maxLength: 200 }
38471
- }
38739
+ routeMeta({
38740
+ surface: "bootstrap",
38741
+ operationId: "auth.register",
38742
+ authPolicy: "public",
38743
+ stability: "preview",
38744
+ summary: "Ask for an account",
38745
+ requestSchema: {
38746
+ type: "object",
38747
+ required: ["email", "challengeId"],
38748
+ properties: {
38749
+ email: { type: "string", minLength: 3, maxLength: 254 },
38750
+ name: { type: "string", maxLength: 200 },
38751
+ note: { type: "string", maxLength: 1e3 },
38752
+ challengeId: { type: "string", minLength: 1, maxLength: 64 },
38753
+ website: { type: "string", maxLength: 200 }
38472
38754
  }
38473
38755
  }
38474
- },
38756
+ }),
38475
38757
  async (req, reply) => {
38476
38758
  const email = (req.body.email ?? "").trim().toLowerCase();
38477
38759
  const name = (req.body.name ?? "").trim() || null;
@@ -38519,7 +38801,13 @@ async function registrationRoutes(app, deps) {
38519
38801
  return reply.status(202).send({ status: "pending" });
38520
38802
  }
38521
38803
  );
38522
- app.get("/auth/register-approve", async (req, reply) => {
38804
+ app.get("/auth/register-approve", routeMeta({
38805
+ surface: "bootstrap",
38806
+ operationId: "auth.registerApprove",
38807
+ authPolicy: "public",
38808
+ stability: "internal",
38809
+ summary: "Approve a registration request from its link"
38810
+ }), async (req, reply) => {
38523
38811
  const token = (req.query.token ?? "").trim();
38524
38812
  const row = token ? await db.get(`SELECT id, email, name, note, status, approve_token as approveToken,
38525
38813
  reject_token as rejectToken, created_at as createdAt,
@@ -38539,7 +38827,13 @@ async function registrationRoutes(app, deps) {
38539
38827
  )
38540
38828
  );
38541
38829
  });
38542
- app.get("/auth/register-reject", async (req, reply) => {
38830
+ app.get("/auth/register-reject", routeMeta({
38831
+ surface: "bootstrap",
38832
+ operationId: "auth.registerReject",
38833
+ authPolicy: "public",
38834
+ stability: "internal",
38835
+ summary: "Reject a registration request from its link"
38836
+ }), async (req, reply) => {
38543
38837
  const token = (req.query.token ?? "").trim();
38544
38838
  const row = token ? await db.get(`SELECT id, email, name, status FROM registration_requests WHERE reject_token = ?`, token) : void 0;
38545
38839
  if (!row) {
@@ -38741,6 +39035,7 @@ function safeNext(next) {
38741
39035
  }
38742
39036
 
38743
39037
  // src/routes/install.ts
39038
+ init_route_registry();
38744
39039
  import { fileURLToPath as fileURLToPath2 } from "node:url";
38745
39040
  import { dirname as dirname3, resolve as resolve2 } from "node:path";
38746
39041
  import { existsSync as existsSync6 } from "node:fs";
@@ -38751,7 +39046,14 @@ var SCRIPT_PATH = [
38751
39046
  resolve2(MODULE_DIR2, "../../../deploy/install-sandbox.sh")
38752
39047
  ].find((p) => existsSync6(p)) ?? resolve2(MODULE_DIR2, "../../../deploy/install-sandbox.sh");
38753
39048
  async function installRoutes(app) {
38754
- app.get("/install-sandbox.sh", async (_req, reply) => {
39049
+ app.get("/install-sandbox.sh", routeMeta({
39050
+ surface: "static",
39051
+ operationId: "ops.installSandboxScript",
39052
+ authPolicy: "public",
39053
+ // A shell script, not a JSON operation.
39054
+ stability: "internal",
39055
+ summary: "The SANDBOX install script"
39056
+ }), async (_req, reply) => {
38755
39057
  try {
38756
39058
  const body = await readFile(SCRIPT_PATH, "utf8");
38757
39059
  return reply.type("text/x-shellscript; charset=utf-8").send(body);
@@ -39522,10 +39824,18 @@ async function negotiateJobCredentials(db, spec, workerId, log, issuer) {
39522
39824
  // src/routes/jobs.ts
39523
39825
  init_sandbox2();
39524
39826
  init_ws();
39827
+ init_route_registry();
39525
39828
  async function jobsRoutes(app, deps) {
39526
39829
  const { db, config } = deps;
39527
39830
  const notifyDeps = deps.notify;
39528
- app.post("/jobs/claim", async (req, reply) => {
39831
+ app.post("/node/v1/jobs/claim", routeMeta({
39832
+ surface: "node",
39833
+ operationId: "node.jobs.claim",
39834
+ authPolicy: "node_actor",
39835
+ resourcePolicy: "node.connect",
39836
+ stability: "preview",
39837
+ summary: "Claim the next job for this SANDBOX (long poll)"
39838
+ }), async (req, reply) => {
39529
39839
  const sandbox = await authenticateSandboxByBearer(db, req.headers.authorization);
39530
39840
  if (!sandbox) {
39531
39841
  return reply.status(401).send({ error: "unauthorized" });
@@ -39579,8 +39889,16 @@ async function jobsRoutes(app, deps) {
39579
39889
  }
39580
39890
  });
39581
39891
  app.post(
39582
- "/jobs/heartbeat",
39892
+ "/node/v1/jobs/heartbeat",
39583
39893
  {
39894
+ ...routeMeta({
39895
+ surface: "node",
39896
+ operationId: "node.jobs.heartbeat",
39897
+ authPolicy: "node_actor",
39898
+ resourcePolicy: "node.connect",
39899
+ stability: "preview",
39900
+ summary: "Keep a claimed job's lease alive"
39901
+ }),
39584
39902
  schema: {
39585
39903
  body: {
39586
39904
  type: "object",
@@ -39642,8 +39960,16 @@ async function jobsRoutes(app, deps) {
39642
39960
  }
39643
39961
  );
39644
39962
  app.post(
39645
- "/jobs/release",
39963
+ "/node/v1/jobs/release",
39646
39964
  {
39965
+ ...routeMeta({
39966
+ surface: "node",
39967
+ operationId: "node.jobs.release",
39968
+ authPolicy: "node_actor",
39969
+ resourcePolicy: "node.connect",
39970
+ stability: "preview",
39971
+ summary: "Hand a claimed job back to the queue"
39972
+ }),
39647
39973
  schema: {
39648
39974
  body: {
39649
39975
  type: "object",
@@ -39679,8 +40005,16 @@ async function jobsRoutes(app, deps) {
39679
40005
  }
39680
40006
  );
39681
40007
  app.post(
39682
- "/jobs/result",
40008
+ "/node/v1/jobs/result",
39683
40009
  {
40010
+ ...routeMeta({
40011
+ surface: "node",
40012
+ operationId: "node.jobs.result",
40013
+ authPolicy: "node_actor",
40014
+ resourcePolicy: "node.connect",
40015
+ stability: "preview",
40016
+ summary: "Report a finished job"
40017
+ }),
39684
40018
  schema: {
39685
40019
  body: {
39686
40020
  type: "object",
@@ -41454,9 +41788,11 @@ async function buildServer(config, db) {
41454
41788
  }
41455
41789
  });
41456
41790
  }
41457
- const routeRegistry = installRouteGuard(app, "observe", {
41458
- warn: (message) => app.log.warn(message)
41459
- });
41791
+ const routeRegistry = installRouteGuard(app, "enforce", { warn: (message) => app.log.warn(message) }, [
41792
+ ...LEGACY_ROUTES,
41793
+ ...STATIC_PLUGIN_ROUTES
41794
+ ]);
41795
+ app.decorate("routeRegistry", routeRegistry);
41460
41796
  const mailer = buildMailer(loadMailerConfig(), app.log);
41461
41797
  app.log.info({ mailer: mailer.mode }, "mailer mode");
41462
41798
  const notifyDeps = {
@@ -41678,10 +42014,9 @@ async function buildServer(config, db) {
41678
42014
  app.log.info(
41679
42015
  {
41680
42016
  classified: routeRegistry.list().length,
41681
- unclassified: routeRegistry.unclassifiedCount(),
41682
- invalid: routeRegistry.invalidCount()
42017
+ legacy: routeRegistry.unclassifiedRoutes().filter((r) => !STATIC_PLUGIN_ROUTES.includes(r)).length
41683
42018
  },
41684
- "route registry: routes still to declare their metadata"
42019
+ "route registry: legacy routes still served for the browser"
41685
42020
  );
41686
42021
  });
41687
42022
  await registerSpaHosts(app, {