@spfn/core 0.3.0-beta.2 → 0.3.0-beta.20

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 (93) hide show
  1. package/README.md +331 -16
  2. package/dist/app-config/index.d.ts +134 -0
  3. package/dist/app-config/index.js +58 -0
  4. package/dist/app-config/index.js.map +1 -0
  5. package/dist/authz/index.js +1 -381
  6. package/dist/authz/index.js.map +1 -1
  7. package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
  8. package/dist/codegen/index.d.ts +93 -15
  9. package/dist/codegen/index.js +626 -165
  10. package/dist/codegen/index.js.map +1 -1
  11. package/dist/config/index.d.ts +0 -63
  12. package/dist/config/index.js +15 -15
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/contract/index.d.ts +40 -17
  15. package/dist/contract/index.js +147 -23
  16. package/dist/contract/index.js.map +1 -1
  17. package/dist/db/index.d.ts +201 -34
  18. package/dist/db/index.js +294 -158
  19. package/dist/db/index.js.map +1 -1
  20. package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
  21. package/dist/env/index.d.ts +6 -442
  22. package/dist/env/index.js +13 -16
  23. package/dist/env/index.js.map +1 -1
  24. package/dist/env/loader.js +24 -1
  25. package/dist/env/loader.js.map +1 -1
  26. package/dist/error-BcdBVfRA.d.ts +150 -0
  27. package/dist/errors/index.js +1 -381
  28. package/dist/errors/index.js.map +1 -1
  29. package/dist/event/index.d.ts +5 -5
  30. package/dist/event/index.js +32 -1
  31. package/dist/event/index.js.map +1 -1
  32. package/dist/event/sse/client.d.ts +18 -4
  33. package/dist/event/sse/client.js +38 -2
  34. package/dist/event/sse/client.js.map +1 -1
  35. package/dist/event/sse/index.d.ts +5 -4
  36. package/dist/event/ws/client.d.ts +3 -2
  37. package/dist/event/ws/index.d.ts +4 -3
  38. package/dist/job/index.d.ts +48 -3
  39. package/dist/job/index.js +131 -52
  40. package/dist/job/index.js.map +1 -1
  41. package/dist/logger/index.js +0 -12
  42. package/dist/logger/index.js.map +1 -1
  43. package/dist/middleware/index.d.ts +6 -2
  44. package/dist/middleware/index.js +7 -388
  45. package/dist/middleware/index.js.map +1 -1
  46. package/dist/nextjs/index.d.ts +88 -8
  47. package/dist/nextjs/index.js +138 -62
  48. package/dist/nextjs/index.js.map +1 -1
  49. package/dist/nextjs/server.d.ts +45 -7
  50. package/dist/nextjs/server.js +14 -0
  51. package/dist/nextjs/server.js.map +1 -1
  52. package/dist/ops/index.d.ts +59 -43
  53. package/dist/ops/index.js +543 -97
  54. package/dist/ops/index.js.map +1 -1
  55. package/dist/registry-C2KQCh0m.d.ts +473 -0
  56. package/dist/route/index.d.ts +5 -3
  57. package/dist/route/index.js +48 -28
  58. package/dist/route/index.js.map +1 -1
  59. package/dist/{router-Qbssr11H.d.ts → route-builder-BHKgGkwI.d.ts} +3 -126
  60. package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
  61. package/dist/router-DaDrprJd.d.ts +172 -0
  62. package/dist/server/index.d.ts +185 -14
  63. package/dist/server/index.js +834 -78
  64. package/dist/server/index.js.map +1 -1
  65. package/dist/surface/index.d.ts +109 -0
  66. package/dist/surface/index.js +908 -0
  67. package/dist/surface/index.js.map +1 -0
  68. package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
  69. package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
  70. package/dist/types-CxhPH4s_.d.ts +375 -0
  71. package/dist/{types-D1c57Ko-.d.ts → types-D52SokSM.d.ts} +43 -4
  72. package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
  73. package/docs/file-upload.md +195 -333
  74. package/package.json +20 -9
  75. package/src/cache/README.md +330 -0
  76. package/src/codegen/README.md +634 -0
  77. package/src/config/README.md +333 -0
  78. package/src/contract/README.md +440 -0
  79. package/src/db/README.md +589 -0
  80. package/src/db/manager/README.md +500 -0
  81. package/src/db/schema/README.md +354 -0
  82. package/src/db/transaction/README.md +822 -0
  83. package/src/env/README.md +787 -0
  84. package/src/errors/README.md +429 -0
  85. package/src/event/README.md +789 -0
  86. package/src/job/README.md +613 -0
  87. package/src/logger/README.md +321 -0
  88. package/src/middleware/README.md +634 -0
  89. package/src/nextjs/README.md +688 -0
  90. package/src/route/README.md +763 -0
  91. package/src/security/README.md +100 -0
  92. package/src/server/README.md +704 -0
  93. package/dist/types-9oszaJqp.d.ts +0 -250
package/dist/ops/index.js CHANGED
@@ -138,7 +138,7 @@ var RouteBuilder = class _RouteBuilder {
138
138
  * @example
139
139
  * ```ts
140
140
  * // Skip specific middlewares
141
- * route.get('/health')
141
+ * route.get('/status')
142
142
  * .skip(['auth', 'rateLimit'])
143
143
  * .handler(async (c) => c.json({ status: 'ok' }));
144
144
  *
@@ -268,43 +268,33 @@ var route = {
268
268
  };
269
269
 
270
270
  // src/route/router.ts
271
- function createRouterInstance(routes, packageRouters = [], globalMiddlewares = [], contractVersion = null) {
271
+ function createRouterInstance(state) {
272
272
  return {
273
- routes,
274
- _routes: routes,
275
- _packageRouters: packageRouters,
276
- _globalMiddlewares: globalMiddlewares,
277
- _contractVersion: contractVersion,
273
+ routes: state.routes,
274
+ _routes: state.routes,
275
+ _packageRouters: state.packageRouters,
276
+ _globalMiddlewares: state.globalMiddlewares,
277
+ _contractVersion: state.contractVersion,
278
+ _publishesRouteMap: state.publishesRouteMap,
278
279
  packages(routers) {
279
- const newPackageRouters = [...this._packageRouters, ...routers];
280
+ const newPackageRouters = [...state.packageRouters, ...routers];
280
281
  for (const pkgRouter of routers) {
281
282
  if (pkgRouter._packageRouters?.length > 0) {
282
283
  newPackageRouters.push(...pkgRouter._packageRouters);
283
284
  }
284
285
  }
285
- return createRouterInstance(
286
- this.routes,
287
- newPackageRouters,
288
- this._globalMiddlewares,
289
- this._contractVersion
290
- );
286
+ assertDistinctSurfaces(newPackageRouters);
287
+ return createRouterInstance({ ...state, packageRouters: newPackageRouters });
291
288
  },
292
289
  use(middlewares) {
293
- return createRouterInstance(
294
- this.routes,
295
- this._packageRouters,
296
- [...this._globalMiddlewares, ...middlewares],
297
- this._contractVersion
298
- );
290
+ return createRouterInstance({
291
+ ...state,
292
+ globalMiddlewares: [...state.globalMiddlewares, ...middlewares]
293
+ });
299
294
  },
300
295
  contractVersion(version) {
301
296
  assertContractVersion(version);
302
- return createRouterInstance(
303
- this.routes,
304
- this._packageRouters,
305
- this._globalMiddlewares,
306
- version
307
- );
297
+ return createRouterInstance({ ...state, contractVersion: version });
308
298
  }
309
299
  };
310
300
  }
@@ -315,17 +305,50 @@ function assertContractVersion(version) {
315
305
  );
316
306
  }
317
307
  }
318
- function defineRouter(routes) {
319
- return createRouterInstance(routes);
308
+ function assertDistinctSurfaces(packageRouters) {
309
+ const namespaces = /* @__PURE__ */ new Set();
310
+ for (const router of packageRouters) {
311
+ const namespace = router._surfaceNamespace;
312
+ if (namespace === void 0) {
313
+ continue;
314
+ }
315
+ if (namespaces.has(namespace)) {
316
+ throw new Error(
317
+ `.packages() mounts two command surfaces on "${namespace}". Both serve ${namespace}/_manifest, so the second one's commands would answer while no manifest lists them. Mount one router per namespace.`
318
+ );
319
+ }
320
+ namespaces.add(namespace);
321
+ }
322
+ }
323
+ function defineUnmappedRouter(routes) {
324
+ return createRouterInstance({
325
+ routes,
326
+ packageRouters: [],
327
+ globalMiddlewares: [],
328
+ contractVersion: null,
329
+ publishesRouteMap: false
330
+ });
320
331
  }
321
332
 
322
- // src/ops/manifest.ts
323
- var OpsRouterError = class extends Error {
333
+ // src/surface/error.ts
334
+ var SurfaceError = class extends Error {
324
335
  constructor(message) {
325
336
  super(message);
326
- this.name = "OpsRouterError";
337
+ this.name = "SurfaceError";
327
338
  }
328
339
  };
340
+
341
+ // src/surface/wording.ts
342
+ var SURFACE_WORDING = {
343
+ noun: "surface",
344
+ builder: "`<surface>.route`",
345
+ factory: "defineSurface"
346
+ };
347
+ function capitalized(noun) {
348
+ return noun.charAt(0).toUpperCase() + noun.slice(1);
349
+ }
350
+
351
+ // src/surface/manifest.ts
329
352
  function isRouter(value) {
330
353
  return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
331
354
  }
@@ -349,22 +372,22 @@ function toCommandInput(input) {
349
372
  }
350
373
  return sections;
351
374
  }
352
- function collectOpsCommands(routes) {
375
+ function collectSurfaceCommands(routes, wording = SURFACE_WORDING) {
353
376
  const commands = [];
354
- visit(routes, commands, /* @__PURE__ */ new Map());
377
+ visit(routes, commands, /* @__PURE__ */ new Map(), wording);
355
378
  commands.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
356
379
  return commands;
357
380
  }
358
- function visit(routes, commands, claimed) {
381
+ function visit(routes, commands, claimed, wording) {
359
382
  for (const [name, entry] of Object.entries(routes)) {
360
383
  if (isRouter(entry)) {
361
- visit(entry.routes, commands, claimed);
384
+ visit(entry.routes, commands, claimed, wording);
362
385
  continue;
363
386
  }
364
387
  if (!isRouteDef(entry) || !entry.method || !entry.path) {
365
388
  continue;
366
389
  }
367
- assertUnclaimedName(name, entry.path, claimed);
390
+ assertUnclaimedName(name, entry.path, claimed, wording);
368
391
  commands.push({
369
392
  name,
370
393
  method: entry.method,
@@ -373,128 +396,551 @@ function visit(routes, commands, claimed) {
373
396
  });
374
397
  }
375
398
  }
376
- function assertUnclaimedName(name, path, claimed) {
399
+ function assertUnclaimedName(name, path, claimed, wording) {
377
400
  const existing = claimed.get(name);
378
401
  if (existing !== void 0) {
379
- throw new OpsRouterError(
380
- `Two ops routes are named "${name}" ("${existing}" and "${path}"). Command names are flattened across nested routers, so each must be unique for the CLI to resolve the one an operator asked for.`
402
+ throw new SurfaceError(
403
+ `Two ${wording.noun} routes are named "${name}" ("${existing}" and "${path}"). Command names are flattened across nested routers, so each must be unique for a client to resolve the one its caller asked for.`
381
404
  );
382
405
  }
383
406
  claimed.set(name, path);
384
407
  }
385
408
 
386
- // src/ops/create-ops-router.ts
387
- var OPS_PATH_PREFIX = "/_ops/";
388
- var OPS_MANIFEST_PATH = "/_ops/_manifest";
389
- var OPS_MANIFEST_NAME = "getOpsManifest";
409
+ // src/surface/route-overlap.ts
410
+ function routeSegments(path) {
411
+ return path.split("/").slice(1).map((raw) => {
412
+ if (raw === "*" || raw.endsWith("*")) {
413
+ return { kind: "wildcard", value: raw, optional: true };
414
+ }
415
+ if (raw.startsWith(":")) {
416
+ return {
417
+ kind: "parameter",
418
+ value: raw.slice(1).replace(/\?$/, ""),
419
+ optional: raw.endsWith("?")
420
+ };
421
+ }
422
+ return { kind: "static", value: raw, optional: false };
423
+ });
424
+ }
425
+ function customPattern(parameter) {
426
+ const openingBrace = parameter.indexOf("{");
427
+ return openingBrace >= 0 && parameter.endsWith("}") ? parameter.slice(openingBrace + 1, -1) : null;
428
+ }
429
+ function parameterAccepts(parameter, value) {
430
+ const pattern = customPattern(parameter);
431
+ if (pattern === null) {
432
+ return value.length > 0 && !value.includes("/");
433
+ }
434
+ try {
435
+ return new RegExp(`^(?:${pattern})$`).test(value);
436
+ } catch {
437
+ return true;
438
+ }
439
+ }
440
+ function remainingStaticPath(segments, from) {
441
+ const remaining = segments.slice(from);
442
+ return remaining.every((segment) => segment.kind === "static") ? remaining.map((segment) => segment.value).join("/") : null;
443
+ }
444
+ function routePatternsOverlap(firstPath, secondPath) {
445
+ const first = routeSegments(firstPath);
446
+ const second = routeSegments(secondPath);
447
+ const length = Math.max(first.length, second.length);
448
+ for (let index = 0; index < length; index++) {
449
+ const left = first[index];
450
+ const right = second[index];
451
+ if (!left || !right) {
452
+ const remaining = left ? first.slice(index) : second.slice(index);
453
+ return remaining.every((segment) => segment.optional || segment.kind === "wildcard");
454
+ }
455
+ if (left.kind === "wildcard" || right.kind === "wildcard") {
456
+ return true;
457
+ }
458
+ if (left.kind === "static" && right.kind === "static") {
459
+ if (left.value !== right.value) {
460
+ return false;
461
+ }
462
+ continue;
463
+ }
464
+ if (left.kind === "parameter" && right.kind === "static") {
465
+ const remaining = remainingStaticPath(second, index);
466
+ if (customPattern(left.value) !== null && remaining !== null && parameterAccepts(left.value, remaining)) {
467
+ return true;
468
+ }
469
+ if (!parameterAccepts(left.value, right.value)) {
470
+ return customPattern(left.value) !== null && remaining === null;
471
+ }
472
+ if (customPattern(left.value) !== null && first.length !== second.length) {
473
+ return true;
474
+ }
475
+ continue;
476
+ }
477
+ if (left.kind === "static" && right.kind === "parameter") {
478
+ const remaining = remainingStaticPath(first, index);
479
+ if (customPattern(right.value) !== null && remaining !== null && parameterAccepts(right.value, remaining)) {
480
+ return true;
481
+ }
482
+ if (!parameterAccepts(right.value, left.value)) {
483
+ return customPattern(right.value) !== null && remaining === null;
484
+ }
485
+ if (customPattern(right.value) !== null && first.length !== second.length) {
486
+ return true;
487
+ }
488
+ continue;
489
+ }
490
+ return true;
491
+ }
492
+ return true;
493
+ }
494
+
495
+ // src/surface/module.ts
496
+ var MODULE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
497
+ var COMMAND_NAME = /^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*$/;
498
+ var CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
499
+ var EFFECTS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
500
+ var PATH_BASE = "https://spfn.invalid";
501
+ function assertText(label, value) {
502
+ if (typeof value !== "string" || value.trim().length === 0) {
503
+ throw new SurfaceError(`${label} must be a non-empty string.`);
504
+ }
505
+ }
506
+ function assertStableModulePath(at, moduleId, commandName, path) {
507
+ const label = `${at.label} command "${moduleId}.${commandName}" path "${path}"`;
508
+ const transportPath = path.replace(/[{}?#]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
509
+ let url;
510
+ try {
511
+ url = new URL(transportPath, PATH_BASE);
512
+ } catch {
513
+ throw new SurfaceError(`${label} is not a valid URL path.`);
514
+ }
515
+ if (url.origin !== PATH_BASE || url.search !== "" || url.hash !== "" || url.pathname !== transportPath) {
516
+ throw new SurfaceError(`${label} is not a stable plain absolute path.`);
517
+ }
518
+ const slashCount = (path.match(/\//g) ?? []).length;
519
+ let decoded = path;
520
+ for (let depth = 0; depth < 8; depth++) {
521
+ let next;
522
+ try {
523
+ next = decodeURIComponent(decoded);
524
+ } catch {
525
+ throw new SurfaceError(`${label} contains malformed percent encoding.`);
526
+ }
527
+ if (next.includes("\\") || (next.match(/\//g) ?? []).length !== slashCount || next.split("/").some((segment) => segment === "." || segment === "..")) {
528
+ throw new SurfaceError(`${label} contains encoded path separators or dot segments.`);
529
+ }
530
+ if (next === decoded) {
531
+ return;
532
+ }
533
+ decoded = next;
534
+ }
535
+ throw new SurfaceError(`${label} uses too many percent-encoding layers.`);
536
+ }
537
+ function assertCliCallableModulePath(at, moduleId, commandName, path) {
538
+ const unsupported = path.split("/").slice(1).find((segment) => segment === "*" || segment.includes("*") || (segment.startsWith(":") ? !/^:[A-Za-z0-9_]+$/.test(segment) : !/^[A-Za-z0-9._~-]+$/.test(segment)));
539
+ if (unsupported !== void 0) {
540
+ throw new SurfaceError(
541
+ `${at.label} command "${moduleId}.${commandName}" path segment "${unsupported}" is not CLI-callable. Module routes support URL-safe static segments and simple :name parameters only; optional, wildcard, and custom-regex parameters are not supported.`
542
+ );
543
+ }
544
+ }
545
+ function assertCommand(at, moduleId, name, command) {
546
+ if (!COMMAND_NAME.test(name)) {
547
+ throw new SurfaceError(
548
+ `${at.label} module "${moduleId}" has invalid command name "${name}". Use dot-separated alphanumeric names.`
549
+ );
550
+ }
551
+ assertText(`${at.label} command "${moduleId}.${name}" summary`, command?.summary);
552
+ if (!EFFECTS.has(command?.effect)) {
553
+ throw new SurfaceError(
554
+ `${at.label} command "${moduleId}.${name}" has invalid effect ${JSON.stringify(command?.effect)}.`
555
+ );
556
+ }
557
+ if (!Array.isArray(command?.scopes) || command.scopes.length === 0 || command.scopes.some((scope) => typeof scope !== "string" || scope.trim().length === 0)) {
558
+ throw new SurfaceError(`${at.label} command "${moduleId}.${name}" must declare at least one non-empty scope.`);
559
+ }
560
+ const route2 = command?.route;
561
+ if (!route2 || typeof route2.handler !== "function" || !route2.method || !route2.path) {
562
+ throw new SurfaceError(`${at.label} command "${moduleId}.${name}" must carry a complete route.`);
563
+ }
564
+ assertStableModulePath(at, moduleId, name, route2.path);
565
+ assertCliCallableModulePath(at, moduleId, name, route2.path);
566
+ const modulePath = `${at.namespace}/${moduleId}/`;
567
+ if (!route2.path.startsWith(modulePath)) {
568
+ throw new SurfaceError(
569
+ `${at.label} command "${moduleId}.${name}" is at "${route2.path}", outside "${modulePath}". A module mounted on "${at.namespace}" keeps every command under "${modulePath}".`
570
+ );
571
+ }
572
+ if (route2.path.split("/").includes("..")) {
573
+ throw new SurfaceError(`${at.label} command "${moduleId}.${name}" path climbs out of its module namespace.`);
574
+ }
575
+ }
576
+ function validateSurfaceModule(module, namespace, wording = SURFACE_WORDING) {
577
+ const at = { namespace, label: capitalized(wording.noun) };
578
+ if (!MODULE_ID.test(module?.id ?? "")) {
579
+ throw new SurfaceError(
580
+ `${at.label} module id ${JSON.stringify(module?.id)} is invalid. Use lower-kebab-case.`
581
+ );
582
+ }
583
+ assertText(`${at.label} module "${module.id}" source`, module.source);
584
+ assertText(`${at.label} module "${module.id}" summary`, module.summary);
585
+ if (!CONTRACT_VERSION.test(module.contractVersion)) {
586
+ throw new SurfaceError(
587
+ `${at.label} module "${module.id}" contractVersion must be a semantic version.`
588
+ );
589
+ }
590
+ if (!module.commands || typeof module.commands !== "object" || Array.isArray(module.commands)) {
591
+ throw new SurfaceError(`${at.label} module "${module.id}" commands must be an object.`);
592
+ }
593
+ const signatures = /* @__PURE__ */ new Map();
594
+ const claimedRoutes = [];
595
+ for (const [name, command] of Object.entries(module.commands)) {
596
+ assertCommand(at, module.id, name, command);
597
+ const signature = `${command.route.method} ${command.route.path}`;
598
+ const existing = signatures.get(signature);
599
+ if (existing) {
600
+ throw new SurfaceError(
601
+ `${at.label} module "${module.id}" commands "${existing}" and "${name}" both use ${signature}.`
602
+ );
603
+ }
604
+ signatures.set(signature, name);
605
+ const overlapping = claimedRoutes.find((claim) => claim.method === command.route.method && routePatternsOverlap(claim.path, command.route.path));
606
+ if (overlapping) {
607
+ throw new SurfaceError(
608
+ `${at.label} module "${module.id}" commands "${overlapping.name}" and "${name}" have overlapping ${command.route.method} routes ("${overlapping.path}" and "${command.route.path}").`
609
+ );
610
+ }
611
+ claimedRoutes.push({
612
+ name,
613
+ method: command.route.method,
614
+ path: command.route.path
615
+ });
616
+ }
617
+ return module;
618
+ }
619
+
620
+ // src/surface/namespace.ts
621
+ var NAMESPACE = /^(?:\/[A-Za-z0-9_-]+)+$/;
622
+ function assertNamespace(namespace) {
623
+ if (typeof namespace !== "string" || !NAMESPACE.test(namespace)) {
624
+ throw new SurfaceError(
625
+ `Surface namespace ${JSON.stringify(namespace)} is invalid. Use an absolute path of [A-Za-z0-9_-] segments without a trailing slash, such as "/_app".`
626
+ );
627
+ }
628
+ }
629
+ function manifestPathOf(namespace) {
630
+ return `${namespace}/_manifest`;
631
+ }
632
+ function manifestNameOf(namespace) {
633
+ return `get${namespace.slice(1).replace(/_[a-z]|[A-Z_/-]/g, encodedCharacter)}Manifest`;
634
+ }
635
+ var ESCAPES = { "_": "_", "-": "d", "/": "s" };
636
+ function encodedCharacter(match) {
637
+ if (match.length === 2) {
638
+ return match.charAt(1).toUpperCase();
639
+ }
640
+ return `_${ESCAPES[match] ?? match}`;
641
+ }
642
+ function toSurfacePath(namespace, path, wording) {
643
+ const label = capitalized(wording.noun);
644
+ if (!path.startsWith("/")) {
645
+ throw new SurfaceError(
646
+ `${label} route path "${path}" must start with "/". It is appended to "${namespace}", so "${path}" would read as "${namespace}${path}".`
647
+ );
648
+ }
649
+ if (path === "/") {
650
+ throw new SurfaceError(
651
+ `${label} route path "/" names no command \u2014 "${namespace}" itself is not a command.`
652
+ );
653
+ }
654
+ return namespace + path;
655
+ }
656
+ function surfaceRouteBuilder(namespace, wording) {
657
+ const method = (name) => (path) => route[name](toSurfacePath(namespace, path, wording));
658
+ return {
659
+ get: method("get"),
660
+ post: method("post"),
661
+ put: method("put"),
662
+ patch: method("patch"),
663
+ delete: method("delete")
664
+ };
665
+ }
666
+
667
+ // src/surface/define-surface.ts
390
668
  function isRouter2(value) {
391
669
  return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
392
670
  }
393
671
  function isRouteDef2(value) {
394
672
  return value !== null && typeof value === "object" && "handler" in value;
395
673
  }
396
- function assertOpsRoute(name, def) {
674
+ function assertSurfaceRoute(plan, name, def) {
675
+ const label = capitalized(plan.wording.noun);
397
676
  if (!def.method || !def.path) {
398
- throw new OpsRouterError(
399
- `Ops route "${name}" has no method or path. An ops command is invoked on the wire, so both are required.`
677
+ throw new SurfaceError(
678
+ `${label} route "${name}" has no method or path. A command is invoked on the wire, so both are required.`
400
679
  );
401
680
  }
402
- if (!def.path.startsWith(OPS_PATH_PREFIX)) {
403
- throw new OpsRouterError(
404
- `Ops route "${name}" is at "${def.path}", outside "${OPS_PATH_PREFIX}". Build ops routes with \`opsRoute\` rather than \`route\` \u2014 it applies the namespace, so the path a definition carries is only the part the app owns.`
681
+ if (!def.path.startsWith(plan.prefix)) {
682
+ throw new SurfaceError(
683
+ `${label} route "${name}" is at "${def.path}", outside "${plan.prefix}". Build ${plan.wording.noun} routes with ${plan.wording.builder} rather than \`route\` \u2014 it applies the namespace, so the path a definition carries is only the part the app owns.`
405
684
  );
406
685
  }
407
- if (def.path === OPS_MANIFEST_PATH) {
408
- throw new OpsRouterError(
409
- `Ops route "${name}" claims "${OPS_MANIFEST_PATH}", which is reserved for the manifest. The manifest is registered first, so this route would never answer.`
686
+ if (def.path === plan.manifestPath) {
687
+ throw new SurfaceError(
688
+ `${label} route "${name}" claims "${plan.manifestPath}", which is reserved for the manifest. The manifest is registered first, so this route would never answer.`
410
689
  );
411
690
  }
412
691
  }
413
- function assertOpsName(name) {
414
- if (name === OPS_MANIFEST_NAME) {
415
- throw new OpsRouterError(
416
- `Ops route name "${OPS_MANIFEST_NAME}" is reserved for the manifest route.`
692
+ function assertSurfaceName(plan, name) {
693
+ if (name === plan.manifestName) {
694
+ throw new SurfaceError(
695
+ `${capitalized(plan.wording.noun)} route name "${plan.manifestName}" is reserved for the manifest route.`
417
696
  );
418
697
  }
419
698
  }
420
- function rebuildNestedRouter(name, router, auth, inherited) {
699
+ function rebuildNestedRouter(plan, name, router, guards, inherited) {
421
700
  if (router._packageRouters?.length > 0) {
422
- throw new OpsRouterError(
423
- `Ops router "${name}" mounts package routers with .packages(). Their routes bypass the prefix check and the auth injection, so an ops surface cannot carry them.`
701
+ throw new SurfaceError(
702
+ `${capitalized(plan.wording.noun)} router "${name}" mounts package routers with .packages(). Their routes bypass the prefix check and the auth injection, so a surface cannot carry them.`
424
703
  );
425
704
  }
426
705
  const handedDown = [...inherited, ...router._globalMiddlewares ?? []];
427
- let rebuilt = defineRouter(
428
- secureRoutes(router.routes, auth, handedDown)
706
+ let rebuilt = defineUnmappedRouter(
707
+ secureRoutes(plan, router.routes, guards, handedDown)
429
708
  );
430
709
  if (router._contractVersion) {
431
710
  rebuilt = rebuilt.contractVersion(router._contractVersion);
432
711
  }
433
712
  return rebuilt;
434
713
  }
435
- function secureRoutes(routes, auth, inherited = []) {
714
+ function secureRoutes(plan, routes, guards, inherited = []) {
436
715
  const secured = {};
437
716
  for (const [name, entry] of Object.entries(routes)) {
438
- assertOpsName(name);
717
+ assertSurfaceName(plan, name);
439
718
  if (isRouter2(entry)) {
440
- secured[name] = rebuildNestedRouter(name, entry, auth, inherited);
719
+ secured[name] = rebuildNestedRouter(plan, name, entry, guards, inherited);
441
720
  continue;
442
721
  }
443
722
  if (!isRouteDef2(entry)) {
444
- throw new OpsRouterError(`Ops router entry "${name}" is neither a route nor a router.`);
723
+ throw new SurfaceError(
724
+ `${capitalized(plan.wording.noun)} router entry "${name}" is neither a route nor a router.`
725
+ );
445
726
  }
446
- assertOpsRoute(name, entry);
727
+ assertSurfaceRoute(plan, name, entry);
447
728
  secured[name] = {
448
729
  ...entry,
449
- middlewares: [auth, ...inherited, ...entry.middlewares ?? []]
730
+ middlewares: commandChain(plan, name, guards.get(name), [...inherited, ...entry.middlewares ?? []])
450
731
  };
451
732
  }
452
733
  return secured;
453
734
  }
454
- function createOpsRouter(routes, options) {
455
- if (!options?.auth) {
456
- throw new OpsRouterError(
457
- "createOpsRouter requires an auth middleware ({ auth: ... }). An ops surface reachable without authentication cannot be created."
735
+ function commandChain(plan, name, guard, declared) {
736
+ if (guard === void 0) {
737
+ return [plan.auth, ...declared];
738
+ }
739
+ const guardName = middlewareNameOf(guard);
740
+ if (guardName !== void 0) {
741
+ refuseGuardCollision(plan, name, guardName, declared);
742
+ }
743
+ return [plan.auth, guard, ...declared];
744
+ }
745
+ function refuseGuardCollision(plan, name, guardName, declared) {
746
+ const noun = capitalized(plan.wording.noun);
747
+ if (middlewareNameOf(plan.auth) === guardName) {
748
+ throw new SurfaceError(
749
+ `${noun} command "${name}"'s guard shares the name "${guardName}" with the surface's auth middleware. Route registration keeps one middleware per name, so the command's guard would never run. Give one of the two a distinct name.`
750
+ );
751
+ }
752
+ if (declared.some((middleware) => middlewareNameOf(middleware) === guardName)) {
753
+ throw new SurfaceError(
754
+ `${noun} command "${name}" declares a middleware named "${guardName}", the name of the guard authorize gave it. Route registration keeps one middleware per name, so the command's own guard would never run. Give one of the two a distinct name.`
755
+ );
756
+ }
757
+ }
758
+ function middlewareNameOf(middleware) {
759
+ return typeof middleware === "function" ? void 0 : middleware.name;
760
+ }
761
+ function guardsFor(plan, commands) {
762
+ return new Map(commands.map((command) => [command.name, plan.authorize?.(command)]));
763
+ }
764
+ function assertModuleCommandFree(plan, command, appCommands, claimed) {
765
+ const noun = plan.wording.noun;
766
+ if (claimed.names.has(command.name)) {
767
+ throw new SurfaceError(`Two ${noun} commands are named "${command.name}".`);
768
+ }
769
+ const signature = `${command.method} ${command.path}`;
770
+ const existing = claimed.signatures.get(signature);
771
+ if (existing) {
772
+ throw new SurfaceError(
773
+ `${capitalized(noun)} commands "${existing}" and "${command.name}" both use ${signature}.`
458
774
  );
459
775
  }
776
+ const overlapping = appCommands.find((app) => app.method === command.method && routePatternsOverlap(app.path, command.path));
777
+ if (overlapping) {
778
+ throw new SurfaceError(
779
+ `App ${noun} command "${overlapping.name}" at ${command.method} ${overlapping.path} overlaps module command "${command.name}" at ${command.method} ${command.path}. Which one answers cannot depend on route registration order.`
780
+ );
781
+ }
782
+ claimed.names.add(command.name);
783
+ claimed.signatures.set(signature, command.name);
784
+ }
785
+ function compileModules(plan, appCommands) {
786
+ const compiled = { descriptors: [], commands: [], routes: {} };
787
+ const moduleIds = /* @__PURE__ */ new Set();
788
+ const claimed = {
789
+ names: new Set(appCommands.map((command) => command.name)),
790
+ signatures: new Map(appCommands.map((command) => [`${command.method} ${command.path}`, command.name]))
791
+ };
792
+ for (const rawModule of plan.modules) {
793
+ const module = validateSurfaceModule(rawModule, plan.namespace, plan.wording);
794
+ if (moduleIds.has(module.id)) {
795
+ throw new SurfaceError(`Two ${plan.wording.noun} modules use id "${module.id}".`);
796
+ }
797
+ moduleIds.add(module.id);
798
+ compiled.descriptors.push({
799
+ id: module.id,
800
+ source: module.source,
801
+ contractVersion: module.contractVersion,
802
+ summary: module.summary
803
+ });
804
+ for (const [localName, definition] of Object.entries(module.commands)) {
805
+ const command = {
806
+ name: `${module.id}.${localName}`,
807
+ module: module.id,
808
+ summary: definition.summary,
809
+ effect: definition.effect,
810
+ scopes: [...definition.scopes],
811
+ method: definition.route.method,
812
+ path: definition.route.path,
813
+ input: collectSurfaceCommands({ [localName]: definition.route }, plan.wording)[0].input
814
+ };
815
+ assertModuleCommandFree(plan, command, appCommands, claimed);
816
+ compiled.commands.push(command);
817
+ compiled.routes[command.name] = { route: definition.route, command };
818
+ }
819
+ }
820
+ compiled.descriptors.sort((a, b) => a.id.localeCompare(b.id));
821
+ compiled.commands.sort((a, b) => a.name.localeCompare(b.name));
822
+ return compiled;
823
+ }
824
+ function secureModuleRoutes(plan, routes) {
825
+ const secured = {};
826
+ for (const [name, definition] of Object.entries(routes)) {
827
+ assertSurfaceName(plan, name);
828
+ assertSurfaceRoute(plan, name, definition.route);
829
+ secured[name] = {
830
+ ...definition.route,
831
+ middlewares: commandChain(
832
+ plan,
833
+ name,
834
+ plan.authorize?.(definition.command),
835
+ definition.route.middlewares ?? []
836
+ )
837
+ };
838
+ }
839
+ return secured;
840
+ }
841
+ function manifestRoute(plan, manifest) {
842
+ const builder = route.get(plan.manifestPath);
843
+ return (plan.manifest === "public" ? builder.skip(["auth"]) : builder.use([plan.auth])).handler(async () => manifest);
844
+ }
845
+ function buildRouter(plan, routes) {
846
+ const appCommands = collectSurfaceCommands(routes, plan.wording);
847
+ const moduleSurface = compileModules(plan, appCommands);
848
+ const commands = [...appCommands, ...moduleSurface.commands].sort((a, b) => a.name.localeCompare(b.name));
460
849
  const manifest = {
461
850
  manifestVersion: 1,
462
- commands: collectOpsCommands(routes)
851
+ ...moduleSurface.descriptors.length > 0 ? { modules: moduleSurface.descriptors } : {},
852
+ commands
853
+ };
854
+ const secured = secureRoutes(plan, routes, guardsFor(plan, appCommands));
855
+ const securedModules = secureModuleRoutes(plan, moduleSurface.routes);
856
+ return Object.defineProperty(defineUnmappedRouter({
857
+ [plan.manifestName]: manifestRoute(plan, manifest),
858
+ ...secured,
859
+ ...securedModules
860
+ }), "_surfaceNamespace", { value: plan.namespace, enumerable: false });
861
+ }
862
+ function planOf(options, wording) {
863
+ assertNamespace(options?.namespace);
864
+ if (!options.auth) {
865
+ throw new SurfaceError(
866
+ `${wording.factory} requires an auth middleware ({ auth: ... }). A surface reachable without authentication cannot be created.`
867
+ );
868
+ }
869
+ const modules = options.modules ?? [];
870
+ if (modules.length > 0 && !options.authorize) {
871
+ throw new SurfaceError(`${wording.factory} requires an authorize function when modules are mounted.`);
872
+ }
873
+ const manifest = options.manifest ?? "authenticated";
874
+ if (manifest !== "authenticated" && manifest !== "public") {
875
+ throw new SurfaceError(
876
+ `${wording.factory} manifest must be 'authenticated' or 'public', got ${JSON.stringify(manifest)}.`
877
+ );
878
+ }
879
+ return {
880
+ namespace: options.namespace,
881
+ prefix: `${options.namespace}/`,
882
+ manifestPath: manifestPathOf(options.namespace),
883
+ manifestName: manifestNameOf(options.namespace),
884
+ auth: options.auth,
885
+ authorize: options.authorize,
886
+ manifest,
887
+ modules,
888
+ wording
463
889
  };
464
- const secured = secureRoutes(routes, options.auth);
465
- const manifestRoute = route.get(OPS_MANIFEST_PATH).use([options.auth]).handler(async () => manifest);
466
- return defineRouter({
467
- [OPS_MANIFEST_NAME]: manifestRoute,
468
- ...secured
469
- });
470
890
  }
891
+ function createSurface(options, wording) {
892
+ const plan = planOf(options, wording);
893
+ return {
894
+ namespace: plan.namespace,
895
+ route: surfaceRouteBuilder(plan.namespace, wording),
896
+ router: (routes) => buildRouter(plan, routes)
897
+ };
898
+ }
899
+
900
+ // src/ops/wording.ts
901
+ var OPS_WORDING = {
902
+ noun: "ops",
903
+ builder: "`opsRoute`",
904
+ factory: "createOpsRouter"
905
+ };
471
906
 
472
907
  // src/ops/ops-route.ts
473
908
  var OPS_PATH_ROOT = "/_ops";
474
- function toOpsPath(path) {
475
- if (!path.startsWith("/")) {
476
- throw new OpsRouterError(
477
- `Ops route path "${path}" must start with "/". It is appended to "${OPS_PATH_ROOT}", so "${path}" would read as "${OPS_PATH_ROOT}${path}".`
478
- );
909
+ var opsRoute = surfaceRouteBuilder(OPS_PATH_ROOT, OPS_WORDING);
910
+
911
+ // src/ops/create-ops-router.ts
912
+ var OPS_PATH_PREFIX = "/_ops/";
913
+ var OPS_MANIFEST_PATH = "/_ops/_manifest";
914
+ function moduleScopeGuard(authorize) {
915
+ if (!authorize) {
916
+ return void 0;
479
917
  }
480
- if (path === "/") {
481
- throw new OpsRouterError(
482
- `Ops route path "/" names no command \u2014 "${OPS_PATH_ROOT}" itself is not a command.`
918
+ return (command) => command.module === void 0 ? void 0 : authorize(...command.scopes ?? []);
919
+ }
920
+ function createOpsRouter(routes, options) {
921
+ if ((options?.modules ?? []).length > 0 && !options.authorize) {
922
+ throw new SurfaceError(
923
+ "createOpsRouter requires an authorize scope factory when modules are mounted."
483
924
  );
484
925
  }
485
- return OPS_PATH_ROOT + path;
926
+ return createSurface({
927
+ namespace: OPS_PATH_ROOT,
928
+ auth: options?.auth,
929
+ authorize: moduleScopeGuard(options?.authorize),
930
+ modules: options?.modules
931
+ }, OPS_WORDING).router(routes);
932
+ }
933
+
934
+ // src/ops/manifest.ts
935
+ function collectOpsCommands(routes) {
936
+ return collectSurfaceCommands(routes, OPS_WORDING);
486
937
  }
487
- function opsMethod(method) {
488
- return (path) => route[method](toOpsPath(path));
938
+
939
+ // src/ops/module.ts
940
+ function defineOpsModule(module) {
941
+ return validateSurfaceModule(module, OPS_PATH_ROOT, OPS_WORDING);
489
942
  }
490
- var opsRoute = {
491
- get: opsMethod("get"),
492
- post: opsMethod("post"),
493
- put: opsMethod("put"),
494
- patch: opsMethod("patch"),
495
- delete: opsMethod("delete")
496
- };
497
943
 
498
- export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OPS_PATH_ROOT, OpsRouterError, collectOpsCommands, createOpsRouter, opsRoute };
944
+ export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OPS_PATH_ROOT, SurfaceError as OpsRouterError, collectOpsCommands, createOpsRouter, defineOpsModule, opsRoute };
499
945
  //# sourceMappingURL=index.js.map
500
946
  //# sourceMappingURL=index.js.map