@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.
- package/README.md +331 -16
- package/dist/app-config/index.d.ts +134 -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/codegen/index.d.ts +93 -15
- package/dist/codegen/index.js +626 -165
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +0 -63
- package/dist/config/index.js +15 -15
- package/dist/config/index.js.map +1 -1
- package/dist/contract/index.d.ts +40 -17
- package/dist/contract/index.js +147 -23
- package/dist/contract/index.js.map +1 -1
- 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/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.js +24 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/error-BcdBVfRA.d.ts +150 -0
- package/dist/errors/index.js +1 -381
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +5 -5
- package/dist/event/index.js +32 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +18 -4
- package/dist/event/sse/client.js +38 -2
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +5 -4
- package/dist/event/ws/client.d.ts +3 -2
- package/dist/event/ws/index.d.ts +4 -3
- package/dist/job/index.d.ts +48 -3
- package/dist/job/index.js +131 -52
- 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 +88 -8
- package/dist/nextjs/index.js +138 -62
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +45 -7
- package/dist/nextjs/server.js +14 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/ops/index.d.ts +59 -43
- package/dist/ops/index.js +543 -97
- package/dist/ops/index.js.map +1 -1
- package/dist/registry-C2KQCh0m.d.ts +473 -0
- package/dist/route/index.d.ts +5 -3
- package/dist/route/index.js +48 -28
- package/dist/route/index.js.map +1 -1
- package/dist/{router-Qbssr11H.d.ts → route-builder-BHKgGkwI.d.ts} +3 -126
- package/dist/{token-manager-BT5EnUAR.d.ts → router-CysBmgv3.d.ts} +81 -127
- package/dist/router-DaDrprJd.d.ts +172 -0
- package/dist/server/index.d.ts +185 -14
- package/dist/server/index.js +834 -78
- package/dist/server/index.js.map +1 -1
- package/dist/surface/index.d.ts +109 -0
- package/dist/surface/index.js +908 -0
- package/dist/surface/index.js.map +1 -0
- package/dist/token-manager-Ckjt2-qq.d.ts +122 -0
- package/dist/{types-ZQODsBft.d.ts → types-B8zAxFsE.d.ts} +2 -1
- package/dist/types-CxhPH4s_.d.ts +375 -0
- package/dist/{types-D1c57Ko-.d.ts → types-D52SokSM.d.ts} +43 -4
- package/dist/{types-2AbaW4Ie.d.ts → types-W4u_S0lF.d.ts} +4 -3
- package/docs/file-upload.md +195 -333
- package/package.json +20 -9
- package/src/cache/README.md +330 -0
- package/src/codegen/README.md +634 -0
- package/src/config/README.md +333 -0
- package/src/contract/README.md +440 -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 +787 -0
- package/src/errors/README.md +429 -0
- package/src/event/README.md +789 -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 +688 -0
- package/src/route/README.md +763 -0
- package/src/security/README.md +100 -0
- package/src/server/README.md +704 -0
- 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('/
|
|
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(
|
|
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 = [...
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
319
|
-
|
|
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/
|
|
323
|
-
var
|
|
333
|
+
// src/surface/error.ts
|
|
334
|
+
var SurfaceError = class extends Error {
|
|
324
335
|
constructor(message) {
|
|
325
336
|
super(message);
|
|
326
|
-
this.name = "
|
|
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
|
|
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
|
|
380
|
-
`Two
|
|
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/
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
|
674
|
+
function assertSurfaceRoute(plan, name, def) {
|
|
675
|
+
const label = capitalized(plan.wording.noun);
|
|
397
676
|
if (!def.method || !def.path) {
|
|
398
|
-
throw new
|
|
399
|
-
|
|
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(
|
|
403
|
-
throw new
|
|
404
|
-
|
|
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 ===
|
|
408
|
-
throw new
|
|
409
|
-
|
|
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
|
|
414
|
-
if (name ===
|
|
415
|
-
throw new
|
|
416
|
-
|
|
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,
|
|
699
|
+
function rebuildNestedRouter(plan, name, router, guards, inherited) {
|
|
421
700
|
if (router._packageRouters?.length > 0) {
|
|
422
|
-
throw new
|
|
423
|
-
|
|
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 =
|
|
428
|
-
secureRoutes(router.routes,
|
|
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,
|
|
714
|
+
function secureRoutes(plan, routes, guards, inherited = []) {
|
|
436
715
|
const secured = {};
|
|
437
716
|
for (const [name, entry] of Object.entries(routes)) {
|
|
438
|
-
|
|
717
|
+
assertSurfaceName(plan, name);
|
|
439
718
|
if (isRouter2(entry)) {
|
|
440
|
-
secured[name] = rebuildNestedRouter(name, entry,
|
|
719
|
+
secured[name] = rebuildNestedRouter(plan, name, entry, guards, inherited);
|
|
441
720
|
continue;
|
|
442
721
|
}
|
|
443
722
|
if (!isRouteDef2(entry)) {
|
|
444
|
-
throw new
|
|
723
|
+
throw new SurfaceError(
|
|
724
|
+
`${capitalized(plan.wording.noun)} router entry "${name}" is neither a route nor a router.`
|
|
725
|
+
);
|
|
445
726
|
}
|
|
446
|
-
|
|
727
|
+
assertSurfaceRoute(plan, name, entry);
|
|
447
728
|
secured[name] = {
|
|
448
729
|
...entry,
|
|
449
|
-
middlewares:
|
|
730
|
+
middlewares: commandChain(plan, name, guards.get(name), [...inherited, ...entry.middlewares ?? []])
|
|
450
731
|
};
|
|
451
732
|
}
|
|
452
733
|
return secured;
|
|
453
734
|
}
|
|
454
|
-
function
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
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
|
-
|
|
488
|
-
|
|
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
|