@spfn/core 0.3.0-beta.1 → 0.3.0-beta.10
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/README.md +301 -26
- package/dist/app-config/index.d.ts +96 -0
- package/dist/app-config/index.js +58 -0
- package/dist/app-config/index.js.map +1 -0
- package/dist/authz/index.js +1 -381
- package/dist/authz/index.js.map +1 -1
- package/dist/{boss-D16fO2oG.d.ts → boss-BiDCbbAQ.d.ts} +60 -7
- package/dist/config/index.d.ts +0 -63
- package/dist/config/index.js +12 -12
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +5 -4
- package/dist/db/index.d.ts +201 -34
- package/dist/db/index.js +294 -158
- package/dist/db/index.js.map +1 -1
- package/dist/{define-middleware-DfDP39Nq.d.ts → define-middleware-CVKgqo8S.d.ts} +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js +1 -381
- package/dist/errors/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +15 -2
- package/dist/event/sse/client.js +38 -2
- package/dist/event/sse/client.js.map +1 -1
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +118 -40
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js +0 -12
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +6 -2
- package/dist/middleware/index.js +7 -388
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +37 -1
- package/dist/nextjs/index.js +40 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +34 -1
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +115 -14
- package/dist/ops/index.js +339 -15
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +5 -3
- package/dist/route/index.js +1 -1
- package/dist/route/index.js.map +1 -1
- package/dist/{router-ukNdAZcN.d.ts → route-builder-2ani2jEI.d.ts} +3 -126
- package/dist/router-DJdpwuB6.d.ts +127 -0
- package/dist/server/index.d.ts +130 -11
- package/dist/server/index.js +714 -70
- package/dist/server/index.js.map +1 -1
- package/dist/{types-Bvvig_tT.d.ts → types-ClQVomgV.d.ts} +1 -1
- package/docs/file-upload.md +195 -333
- package/package.json +14 -8
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +516 -0
- package/src/config/README.md +326 -0
- package/src/contract/README.md +326 -0
- package/src/db/README.md +589 -0
- package/src/db/manager/README.md +500 -0
- package/src/db/schema/README.md +354 -0
- package/src/db/transaction/README.md +822 -0
- package/src/env/README.md +651 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +741 -0
- package/src/job/README.md +613 -0
- package/src/logger/README.md +321 -0
- package/src/middleware/README.md +634 -0
- package/src/nextjs/README.md +608 -0
- package/src/route/README.md +738 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
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('/
|
|
141
|
+
* route.get('/status')
|
|
142
142
|
* .skip(['auth', 'rateLimit'])
|
|
143
143
|
* .handler(async (c) => c.json({ status: 'ok' }));
|
|
144
144
|
*
|
|
@@ -319,13 +319,15 @@ function defineRouter(routes) {
|
|
|
319
319
|
return createRouterInstance(routes);
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
// src/ops/
|
|
322
|
+
// src/ops/error.ts
|
|
323
323
|
var OpsRouterError = class extends Error {
|
|
324
324
|
constructor(message) {
|
|
325
325
|
super(message);
|
|
326
326
|
this.name = "OpsRouterError";
|
|
327
327
|
}
|
|
328
328
|
};
|
|
329
|
+
|
|
330
|
+
// src/ops/manifest.ts
|
|
329
331
|
function isRouter(value) {
|
|
330
332
|
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
331
333
|
}
|
|
@@ -383,6 +385,242 @@ function assertUnclaimedName(name, path, claimed) {
|
|
|
383
385
|
claimed.set(name, path);
|
|
384
386
|
}
|
|
385
387
|
|
|
388
|
+
// src/ops/ops-route.ts
|
|
389
|
+
var OPS_PATH_ROOT = "/_ops";
|
|
390
|
+
function toOpsPath(path) {
|
|
391
|
+
if (!path.startsWith("/")) {
|
|
392
|
+
throw new OpsRouterError(
|
|
393
|
+
`Ops route path "${path}" must start with "/". It is appended to "${OPS_PATH_ROOT}", so "${path}" would read as "${OPS_PATH_ROOT}${path}".`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
if (path === "/") {
|
|
397
|
+
throw new OpsRouterError(
|
|
398
|
+
`Ops route path "/" names no command \u2014 "${OPS_PATH_ROOT}" itself is not a command.`
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return OPS_PATH_ROOT + path;
|
|
402
|
+
}
|
|
403
|
+
function opsMethod(method) {
|
|
404
|
+
return (path) => route[method](toOpsPath(path));
|
|
405
|
+
}
|
|
406
|
+
var opsRoute = {
|
|
407
|
+
get: opsMethod("get"),
|
|
408
|
+
post: opsMethod("post"),
|
|
409
|
+
put: opsMethod("put"),
|
|
410
|
+
patch: opsMethod("patch"),
|
|
411
|
+
delete: opsMethod("delete")
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// src/ops/route-overlap.ts
|
|
415
|
+
function routeSegments(path) {
|
|
416
|
+
return path.split("/").slice(1).map((raw) => {
|
|
417
|
+
if (raw === "*" || raw.endsWith("*")) {
|
|
418
|
+
return { kind: "wildcard", value: raw, optional: true };
|
|
419
|
+
}
|
|
420
|
+
if (raw.startsWith(":")) {
|
|
421
|
+
return {
|
|
422
|
+
kind: "parameter",
|
|
423
|
+
value: raw.slice(1).replace(/\?$/, ""),
|
|
424
|
+
optional: raw.endsWith("?")
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
return { kind: "static", value: raw, optional: false };
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
function customPattern(parameter) {
|
|
431
|
+
const openingBrace = parameter.indexOf("{");
|
|
432
|
+
return openingBrace >= 0 && parameter.endsWith("}") ? parameter.slice(openingBrace + 1, -1) : null;
|
|
433
|
+
}
|
|
434
|
+
function parameterAccepts(parameter, value) {
|
|
435
|
+
const pattern = customPattern(parameter);
|
|
436
|
+
if (pattern === null) {
|
|
437
|
+
return value.length > 0 && !value.includes("/");
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
return new RegExp(`^(?:${pattern})$`).test(value);
|
|
441
|
+
} catch {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
function remainingStaticPath(segments, from) {
|
|
446
|
+
const remaining = segments.slice(from);
|
|
447
|
+
return remaining.every((segment) => segment.kind === "static") ? remaining.map((segment) => segment.value).join("/") : null;
|
|
448
|
+
}
|
|
449
|
+
function opsRoutePatternsOverlap(firstPath, secondPath) {
|
|
450
|
+
const first = routeSegments(firstPath);
|
|
451
|
+
const second = routeSegments(secondPath);
|
|
452
|
+
const length = Math.max(first.length, second.length);
|
|
453
|
+
for (let index = 0; index < length; index++) {
|
|
454
|
+
const left = first[index];
|
|
455
|
+
const right = second[index];
|
|
456
|
+
if (!left || !right) {
|
|
457
|
+
const remaining = left ? first.slice(index) : second.slice(index);
|
|
458
|
+
return remaining.every((segment) => segment.optional || segment.kind === "wildcard");
|
|
459
|
+
}
|
|
460
|
+
if (left.kind === "wildcard" || right.kind === "wildcard") {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
if (left.kind === "static" && right.kind === "static") {
|
|
464
|
+
if (left.value !== right.value) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (left.kind === "parameter" && right.kind === "static") {
|
|
470
|
+
const remaining = remainingStaticPath(second, index);
|
|
471
|
+
if (customPattern(left.value) !== null && remaining !== null && parameterAccepts(left.value, remaining)) {
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
if (!parameterAccepts(left.value, right.value)) {
|
|
475
|
+
return customPattern(left.value) !== null && remaining === null;
|
|
476
|
+
}
|
|
477
|
+
if (customPattern(left.value) !== null && first.length !== second.length) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (left.kind === "static" && right.kind === "parameter") {
|
|
483
|
+
const remaining = remainingStaticPath(first, index);
|
|
484
|
+
if (customPattern(right.value) !== null && remaining !== null && parameterAccepts(right.value, remaining)) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
if (!parameterAccepts(right.value, left.value)) {
|
|
488
|
+
return customPattern(right.value) !== null && remaining === null;
|
|
489
|
+
}
|
|
490
|
+
if (customPattern(right.value) !== null && first.length !== second.length) {
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/ops/module.ts
|
|
501
|
+
var MODULE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
502
|
+
var COMMAND_NAME = /^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*$/;
|
|
503
|
+
var CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
504
|
+
var EFFECTS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
505
|
+
var OPS_PATH_BASE = "https://spfn.invalid";
|
|
506
|
+
function assertText(label, value) {
|
|
507
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
508
|
+
throw new OpsRouterError(`${label} must be a non-empty string.`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function assertStableModulePath(moduleId, commandName, path) {
|
|
512
|
+
const label = `Ops command "${moduleId}.${commandName}" path "${path}"`;
|
|
513
|
+
const transportPath = path.replace(/[{}?#]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
514
|
+
let url;
|
|
515
|
+
try {
|
|
516
|
+
url = new URL(transportPath, OPS_PATH_BASE);
|
|
517
|
+
} catch {
|
|
518
|
+
throw new OpsRouterError(`${label} is not a valid URL path.`);
|
|
519
|
+
}
|
|
520
|
+
if (url.origin !== OPS_PATH_BASE || url.search !== "" || url.hash !== "" || url.pathname !== transportPath) {
|
|
521
|
+
throw new OpsRouterError(`${label} is not a stable plain absolute path.`);
|
|
522
|
+
}
|
|
523
|
+
const slashCount = (path.match(/\//g) ?? []).length;
|
|
524
|
+
let decoded = path;
|
|
525
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
526
|
+
let next;
|
|
527
|
+
try {
|
|
528
|
+
next = decodeURIComponent(decoded);
|
|
529
|
+
} catch {
|
|
530
|
+
throw new OpsRouterError(`${label} contains malformed percent encoding.`);
|
|
531
|
+
}
|
|
532
|
+
if (next.includes("\\") || (next.match(/\//g) ?? []).length !== slashCount || next.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
533
|
+
throw new OpsRouterError(`${label} contains encoded path separators or dot segments.`);
|
|
534
|
+
}
|
|
535
|
+
if (next === decoded) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
decoded = next;
|
|
539
|
+
}
|
|
540
|
+
throw new OpsRouterError(`${label} uses too many percent-encoding layers.`);
|
|
541
|
+
}
|
|
542
|
+
function assertCliCallableModulePath(moduleId, commandName, path) {
|
|
543
|
+
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)));
|
|
544
|
+
if (unsupported !== void 0) {
|
|
545
|
+
throw new OpsRouterError(
|
|
546
|
+
`Ops 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.`
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function assertCommand(moduleId, name, command) {
|
|
551
|
+
if (!COMMAND_NAME.test(name)) {
|
|
552
|
+
throw new OpsRouterError(
|
|
553
|
+
`Ops module "${moduleId}" has invalid command name "${name}". Use dot-separated alphanumeric names.`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
assertText(`Ops command "${moduleId}.${name}" summary`, command?.summary);
|
|
557
|
+
if (!EFFECTS.has(command?.effect)) {
|
|
558
|
+
throw new OpsRouterError(
|
|
559
|
+
`Ops command "${moduleId}.${name}" has invalid effect ${JSON.stringify(command?.effect)}.`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
if (!Array.isArray(command?.scopes) || command.scopes.length === 0 || command.scopes.some((scope) => typeof scope !== "string" || scope.trim().length === 0)) {
|
|
563
|
+
throw new OpsRouterError(`Ops command "${moduleId}.${name}" must declare at least one non-empty scope.`);
|
|
564
|
+
}
|
|
565
|
+
const route2 = command?.route;
|
|
566
|
+
if (!route2 || typeof route2.handler !== "function" || !route2.method || !route2.path) {
|
|
567
|
+
throw new OpsRouterError(`Ops command "${moduleId}.${name}" must carry a complete ops route.`);
|
|
568
|
+
}
|
|
569
|
+
assertStableModulePath(moduleId, name, route2.path);
|
|
570
|
+
assertCliCallableModulePath(moduleId, name, route2.path);
|
|
571
|
+
const modulePath = `${OPS_PATH_ROOT}/${moduleId}/`;
|
|
572
|
+
if (!route2.path.startsWith(modulePath)) {
|
|
573
|
+
throw new OpsRouterError(
|
|
574
|
+
`Ops command "${moduleId}.${name}" is at "${route2.path}", outside "${modulePath}".`
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
if (route2.path.split("/").includes("..")) {
|
|
578
|
+
throw new OpsRouterError(`Ops command "${moduleId}.${name}" path climbs out of its module namespace.`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function defineOpsModule(module) {
|
|
582
|
+
if (!MODULE_ID.test(module?.id ?? "")) {
|
|
583
|
+
throw new OpsRouterError(
|
|
584
|
+
`Ops module id ${JSON.stringify(module?.id)} is invalid. Use lower-kebab-case.`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
assertText(`Ops module "${module.id}" source`, module.source);
|
|
588
|
+
assertText(`Ops module "${module.id}" summary`, module.summary);
|
|
589
|
+
if (!CONTRACT_VERSION.test(module.contractVersion)) {
|
|
590
|
+
throw new OpsRouterError(
|
|
591
|
+
`Ops module "${module.id}" contractVersion must be a semantic version.`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (!module.commands || typeof module.commands !== "object" || Array.isArray(module.commands)) {
|
|
595
|
+
throw new OpsRouterError(`Ops module "${module.id}" commands must be an object.`);
|
|
596
|
+
}
|
|
597
|
+
const signatures = /* @__PURE__ */ new Map();
|
|
598
|
+
const claimedRoutes = [];
|
|
599
|
+
for (const [name, command] of Object.entries(module.commands)) {
|
|
600
|
+
assertCommand(module.id, name, command);
|
|
601
|
+
const signature = `${command.route.method} ${command.route.path}`;
|
|
602
|
+
const existing = signatures.get(signature);
|
|
603
|
+
if (existing) {
|
|
604
|
+
throw new OpsRouterError(
|
|
605
|
+
`Ops module "${module.id}" commands "${existing}" and "${name}" both use ${signature}.`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
signatures.set(signature, name);
|
|
609
|
+
const overlapping = claimedRoutes.find((claim) => claim.method === command.route.method && opsRoutePatternsOverlap(claim.path, command.route.path));
|
|
610
|
+
if (overlapping) {
|
|
611
|
+
throw new OpsRouterError(
|
|
612
|
+
`Ops module "${module.id}" commands "${overlapping.name}" and "${name}" have overlapping ${command.route.method} routes ("${overlapping.path}" and "${command.route.path}").`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
claimedRoutes.push({
|
|
616
|
+
name,
|
|
617
|
+
method: command.route.method,
|
|
618
|
+
path: command.route.path
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return module;
|
|
622
|
+
}
|
|
623
|
+
|
|
386
624
|
// src/ops/create-ops-router.ts
|
|
387
625
|
var OPS_PATH_PREFIX = "/_ops/";
|
|
388
626
|
var OPS_MANIFEST_PATH = "/_ops/_manifest";
|
|
@@ -401,12 +639,12 @@ function assertOpsRoute(name, def) {
|
|
|
401
639
|
}
|
|
402
640
|
if (!def.path.startsWith(OPS_PATH_PREFIX)) {
|
|
403
641
|
throw new OpsRouterError(
|
|
404
|
-
`Ops route "${name}" is at "${def.path}", outside "${OPS_PATH_PREFIX}".
|
|
642
|
+
`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.`
|
|
405
643
|
);
|
|
406
644
|
}
|
|
407
645
|
if (def.path === OPS_MANIFEST_PATH) {
|
|
408
646
|
throw new OpsRouterError(
|
|
409
|
-
`Ops route "${name}" claims "${OPS_MANIFEST_PATH}", which is reserved for the manifest.`
|
|
647
|
+
`Ops route "${name}" claims "${OPS_MANIFEST_PATH}", which is reserved for the manifest. The manifest is registered first, so this route would never answer.`
|
|
410
648
|
);
|
|
411
649
|
}
|
|
412
650
|
}
|
|
@@ -417,29 +655,27 @@ function assertOpsName(name) {
|
|
|
417
655
|
);
|
|
418
656
|
}
|
|
419
657
|
}
|
|
420
|
-
function rebuildNestedRouter(name, router, auth) {
|
|
658
|
+
function rebuildNestedRouter(name, router, auth, inherited) {
|
|
421
659
|
if (router._packageRouters?.length > 0) {
|
|
422
660
|
throw new OpsRouterError(
|
|
423
661
|
`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.`
|
|
424
662
|
);
|
|
425
663
|
}
|
|
664
|
+
const handedDown = [...inherited, ...router._globalMiddlewares ?? []];
|
|
426
665
|
let rebuilt = defineRouter(
|
|
427
|
-
secureRoutes(router.routes, auth)
|
|
666
|
+
secureRoutes(router.routes, auth, handedDown)
|
|
428
667
|
);
|
|
429
|
-
if (router._globalMiddlewares?.length > 0) {
|
|
430
|
-
rebuilt = rebuilt.use(router._globalMiddlewares);
|
|
431
|
-
}
|
|
432
668
|
if (router._contractVersion) {
|
|
433
669
|
rebuilt = rebuilt.contractVersion(router._contractVersion);
|
|
434
670
|
}
|
|
435
671
|
return rebuilt;
|
|
436
672
|
}
|
|
437
|
-
function secureRoutes(routes, auth) {
|
|
673
|
+
function secureRoutes(routes, auth, inherited = []) {
|
|
438
674
|
const secured = {};
|
|
439
675
|
for (const [name, entry] of Object.entries(routes)) {
|
|
440
676
|
assertOpsName(name);
|
|
441
677
|
if (isRouter2(entry)) {
|
|
442
|
-
secured[name] = rebuildNestedRouter(name, entry, auth);
|
|
678
|
+
secured[name] = rebuildNestedRouter(name, entry, auth, inherited);
|
|
443
679
|
continue;
|
|
444
680
|
}
|
|
445
681
|
if (!isRouteDef2(entry)) {
|
|
@@ -448,7 +684,83 @@ function secureRoutes(routes, auth) {
|
|
|
448
684
|
assertOpsRoute(name, entry);
|
|
449
685
|
secured[name] = {
|
|
450
686
|
...entry,
|
|
451
|
-
middlewares: [auth, ...entry.middlewares ?? []]
|
|
687
|
+
middlewares: [auth, ...inherited, ...entry.middlewares ?? []]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return secured;
|
|
691
|
+
}
|
|
692
|
+
function compileModules(modules, appCommands) {
|
|
693
|
+
const descriptors = [];
|
|
694
|
+
const commands = [];
|
|
695
|
+
const routes = {};
|
|
696
|
+
const moduleIds = /* @__PURE__ */ new Set();
|
|
697
|
+
const commandNames = new Set(appCommands.map((command) => command.name));
|
|
698
|
+
const routeSignatures = new Map(
|
|
699
|
+
appCommands.map((command) => [`${command.method} ${command.path}`, command.name])
|
|
700
|
+
);
|
|
701
|
+
for (const rawModule of modules) {
|
|
702
|
+
const module = defineOpsModule(rawModule);
|
|
703
|
+
if (moduleIds.has(module.id)) {
|
|
704
|
+
throw new OpsRouterError(`Two ops modules use id "${module.id}".`);
|
|
705
|
+
}
|
|
706
|
+
moduleIds.add(module.id);
|
|
707
|
+
descriptors.push({
|
|
708
|
+
id: module.id,
|
|
709
|
+
source: module.source,
|
|
710
|
+
contractVersion: module.contractVersion,
|
|
711
|
+
summary: module.summary
|
|
712
|
+
});
|
|
713
|
+
for (const [localName, definition] of Object.entries(module.commands)) {
|
|
714
|
+
const name = `${module.id}.${localName}`;
|
|
715
|
+
if (commandNames.has(name)) {
|
|
716
|
+
throw new OpsRouterError(`Two ops commands are named "${name}".`);
|
|
717
|
+
}
|
|
718
|
+
commandNames.add(name);
|
|
719
|
+
const method = definition.route.method;
|
|
720
|
+
const path = definition.route.path;
|
|
721
|
+
const signature = `${method} ${path}`;
|
|
722
|
+
const existing = routeSignatures.get(signature);
|
|
723
|
+
if (existing) {
|
|
724
|
+
throw new OpsRouterError(
|
|
725
|
+
`Ops commands "${existing}" and "${name}" both use ${signature}.`
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
const overlappingAppCommand = appCommands.find((command) => command.method === method && opsRoutePatternsOverlap(command.path, path));
|
|
729
|
+
if (overlappingAppCommand) {
|
|
730
|
+
throw new OpsRouterError(
|
|
731
|
+
`App ops command "${overlappingAppCommand.name}" at ${method} ${overlappingAppCommand.path} overlaps module command "${name}" at ${method} ${path}. Which one answers cannot depend on route registration order.`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
routeSignatures.set(signature, name);
|
|
735
|
+
commands.push({
|
|
736
|
+
name,
|
|
737
|
+
module: module.id,
|
|
738
|
+
summary: definition.summary,
|
|
739
|
+
effect: definition.effect,
|
|
740
|
+
scopes: [...definition.scopes],
|
|
741
|
+
method,
|
|
742
|
+
path,
|
|
743
|
+
input: collectOpsCommands({ [localName]: definition.route })[0].input
|
|
744
|
+
});
|
|
745
|
+
routes[name] = { route: definition.route, scopes: definition.scopes };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
descriptors.sort((a, b) => a.id.localeCompare(b.id));
|
|
749
|
+
commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
750
|
+
return { descriptors, commands, routes };
|
|
751
|
+
}
|
|
752
|
+
function secureModuleRoutes(routes, auth, authorize) {
|
|
753
|
+
const secured = {};
|
|
754
|
+
for (const [name, definition] of Object.entries(routes)) {
|
|
755
|
+
assertOpsName(name);
|
|
756
|
+
assertOpsRoute(name, definition.route);
|
|
757
|
+
secured[name] = {
|
|
758
|
+
...definition.route,
|
|
759
|
+
middlewares: [
|
|
760
|
+
auth,
|
|
761
|
+
authorize(...definition.scopes),
|
|
762
|
+
...definition.route.middlewares ?? []
|
|
763
|
+
]
|
|
452
764
|
};
|
|
453
765
|
}
|
|
454
766
|
return secured;
|
|
@@ -459,18 +771,30 @@ function createOpsRouter(routes, options) {
|
|
|
459
771
|
"createOpsRouter requires an auth middleware ({ auth: ... }). An ops surface reachable without authentication cannot be created."
|
|
460
772
|
);
|
|
461
773
|
}
|
|
774
|
+
const modules = options.modules ?? [];
|
|
775
|
+
if (modules.length > 0 && !options.authorize) {
|
|
776
|
+
throw new OpsRouterError(
|
|
777
|
+
"createOpsRouter requires an authorize scope factory when modules are mounted."
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const appCommands = collectOpsCommands(routes);
|
|
781
|
+
const moduleSurface = compileModules(modules, appCommands);
|
|
782
|
+
const commands = [...appCommands, ...moduleSurface.commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
462
783
|
const manifest = {
|
|
463
784
|
manifestVersion: 1,
|
|
464
|
-
|
|
785
|
+
...moduleSurface.descriptors.length > 0 ? { modules: moduleSurface.descriptors } : {},
|
|
786
|
+
commands
|
|
465
787
|
};
|
|
466
788
|
const secured = secureRoutes(routes, options.auth);
|
|
789
|
+
const securedModules = moduleSurface.descriptors.length > 0 ? secureModuleRoutes(moduleSurface.routes, options.auth, options.authorize) : {};
|
|
467
790
|
const manifestRoute = route.get(OPS_MANIFEST_PATH).use([options.auth]).handler(async () => manifest);
|
|
468
791
|
return defineRouter({
|
|
792
|
+
[OPS_MANIFEST_NAME]: manifestRoute,
|
|
469
793
|
...secured,
|
|
470
|
-
|
|
794
|
+
...securedModules
|
|
471
795
|
});
|
|
472
796
|
}
|
|
473
797
|
|
|
474
|
-
export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OpsRouterError, collectOpsCommands, createOpsRouter };
|
|
798
|
+
export { OPS_MANIFEST_PATH, OPS_PATH_PREFIX, OPS_PATH_ROOT, OpsRouterError, collectOpsCommands, createOpsRouter, defineOpsModule, opsRoute };
|
|
475
799
|
//# sourceMappingURL=index.js.map
|
|
476
800
|
//# sourceMappingURL=index.js.map
|