@spfn/core 0.3.0-beta.20 → 0.3.0-beta.22
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 +13 -1
- package/dist/contract/index.d.ts +4 -4
- package/dist/{error-BcdBVfRA.d.ts → error-TsUIjp_x.d.ts} +14 -7
- package/dist/ops/index.d.ts +9 -6
- package/dist/ops/index.js +109 -14
- package/dist/ops/index.js.map +1 -1
- package/dist/route/index.d.ts +5 -4
- package/dist/route/index.js +49 -0
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +7 -1
- package/dist/{route-builder-BHKgGkwI.d.ts → route-builder-DRW-MSCJ.d.ts} +49 -1
- package/dist/{router-DaDrprJd.d.ts → router-4YoIy5fN.d.ts} +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +49 -0
- package/dist/server/index.js.map +1 -1
- package/dist/surface/index.d.ts +11 -5
- package/dist/surface/index.js +109 -14
- package/dist/surface/index.js.map +1 -1
- package/dist/{types-D52SokSM.d.ts → types-5Rl11_Nb.d.ts} +1 -1
- package/package.json +3 -3
package/dist/surface/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { MiddlewareHandler } from 'hono';
|
|
2
2
|
import { a as NamedMiddleware } from '../define-middleware-CVKgqo8S.js';
|
|
3
|
-
import { R as RouteDef } from '../route-builder-
|
|
4
|
-
import { R as Router } from '../router-
|
|
5
|
-
import { c as SurfaceRouteBuilder, d as SurfaceCommand, b as SurfaceModule } from '../error-
|
|
6
|
-
export { S as SurfaceEffect, g as SurfaceError, e as SurfaceManifest, a as SurfaceModuleCommand, f as SurfaceModuleDescriptor, h as collectSurfaceCommands, i as defineSurfaceModule } from '../error-
|
|
3
|
+
import { R as RouteDef } from '../route-builder-DRW-MSCJ.js';
|
|
4
|
+
import { R as Router } from '../router-4YoIy5fN.js';
|
|
5
|
+
import { c as SurfaceRouteBuilder, d as SurfaceCommand, b as SurfaceModule } from '../error-TsUIjp_x.js';
|
|
6
|
+
export { S as SurfaceEffect, g as SurfaceError, e as SurfaceManifest, a as SurfaceModuleCommand, f as SurfaceModuleDescriptor, h as collectSurfaceCommands, i as defineSurfaceModule } from '../error-TsUIjp_x.js';
|
|
7
7
|
import '@sinclair/typebox';
|
|
8
8
|
import 'hono/utils/http-status';
|
|
9
9
|
import '../route/types.js';
|
|
10
|
-
import '../types-
|
|
10
|
+
import '../types-5Rl11_Nb.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Command Surface
|
|
@@ -62,6 +62,9 @@ type SurfaceMiddleware = NamedMiddleware<string> | MiddlewareHandler;
|
|
|
62
62
|
* Build the guard for one command, or `undefined` for none. Called once per
|
|
63
63
|
* command when the router is built; the guard runs after `auth` and before the
|
|
64
64
|
* command's own middlewares.
|
|
65
|
+
*
|
|
66
|
+
* The command is the manifest entry, so its `effect` is there to read — a
|
|
67
|
+
* guard can be stricter for a `destructive` command than for a `read`.
|
|
65
68
|
*/
|
|
66
69
|
type SurfaceAuthorize = (command: SurfaceCommand) => SurfaceMiddleware | undefined;
|
|
67
70
|
interface SurfaceOptions {
|
|
@@ -100,6 +103,9 @@ interface Surface {
|
|
|
100
103
|
*
|
|
101
104
|
* The namespace, the auth middleware, and the `authorize` requirement are
|
|
102
105
|
* checked here; routes and modules when `router()` builds the package router.
|
|
106
|
+
* Every app command must declare its effect with
|
|
107
|
+
* `.effect('read' | 'write' | 'destructive')` — `router()` refuses one that
|
|
108
|
+
* does not, so a client is never handed a command it cannot classify.
|
|
103
109
|
* The router is an ordinary `Router` meant to be mounted with `.packages()`,
|
|
104
110
|
* so its routes stay out of the app's client types exactly like other
|
|
105
111
|
* package routes.
|
package/dist/surface/index.js
CHANGED
|
@@ -7,6 +7,8 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
7
7
|
_middlewares;
|
|
8
8
|
_skipMiddlewares;
|
|
9
9
|
_contract;
|
|
10
|
+
_effect;
|
|
11
|
+
_summary;
|
|
10
12
|
/**
|
|
11
13
|
* Create a new RouteBuilder with copied properties and optional overrides
|
|
12
14
|
*/
|
|
@@ -19,6 +21,8 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
19
21
|
builder._middlewares = overrides?.middlewares ?? this._middlewares;
|
|
20
22
|
builder._skipMiddlewares = overrides?.skipMiddlewares ?? this._skipMiddlewares;
|
|
21
23
|
builder._contract = overrides?.contract ?? this._contract;
|
|
24
|
+
builder._effect = overrides?.effect ?? this._effect;
|
|
25
|
+
builder._summary = overrides?.summary ?? this._summary;
|
|
22
26
|
return builder;
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
@@ -189,6 +193,49 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
189
193
|
contract(contract) {
|
|
190
194
|
return this.clone({ contract });
|
|
191
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Declare what invoking this route does
|
|
198
|
+
*
|
|
199
|
+
* Read by a command surface (`@spfn/core/surface`, `@spfn/core/ops`),
|
|
200
|
+
* which announces it in its manifest so a machine caller can tell a read
|
|
201
|
+
* from a write before it calls — `spfn ops call` asks for `--yes` before a
|
|
202
|
+
* `destructive` command. A surface built with `defineSurface` refuses an
|
|
203
|
+
* app command that does not declare one; on `/_ops` it is optional.
|
|
204
|
+
*
|
|
205
|
+
* Nothing else reads it: codegen, the route map, and the contract are
|
|
206
|
+
* unaffected, and a route outside any surface simply carries it.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* export const pageAdd = app.route.post('/pages')
|
|
211
|
+
* .input({ body: PageAddBody })
|
|
212
|
+
* .effect('write')
|
|
213
|
+
* .summary('Add a page to the blueprint')
|
|
214
|
+
* .handler(async (c) => { ... });
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
effect(effect) {
|
|
218
|
+
return this.clone({ effect });
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Describe this route in one line for a command surface's manifest
|
|
222
|
+
*
|
|
223
|
+
* A surface announces it beside the command in `spfn ops list` and
|
|
224
|
+
* `--describe`, and refuses one that is empty or longer than 500
|
|
225
|
+
* characters. Codegen, the route map, and the contract ignore it. On a
|
|
226
|
+
* module command the module's own summary wins.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* export const pageAdd = app.route.post('/pages')
|
|
231
|
+
* .effect('write')
|
|
232
|
+
* .summary('Add a page to the blueprint')
|
|
233
|
+
* .handler(async (c) => { ... });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
summary(summary) {
|
|
237
|
+
return this.clone({ summary });
|
|
238
|
+
}
|
|
192
239
|
/**
|
|
193
240
|
* Define handler function
|
|
194
241
|
*
|
|
@@ -244,6 +291,8 @@ var RouteBuilder = class _RouteBuilder {
|
|
|
244
291
|
middlewares: this._middlewares,
|
|
245
292
|
skipMiddlewares: this._skipMiddlewares,
|
|
246
293
|
contract: this._contract,
|
|
294
|
+
...this._effect !== void 0 ? { effect: this._effect } : {},
|
|
295
|
+
...this._summary !== void 0 ? { summary: this._summary } : {},
|
|
247
296
|
handler: fn,
|
|
248
297
|
_input: {},
|
|
249
298
|
_interceptor: {},
|
|
@@ -392,6 +441,8 @@ function visit(routes, commands, claimed, wording) {
|
|
|
392
441
|
name,
|
|
393
442
|
method: entry.method,
|
|
394
443
|
path: entry.path,
|
|
444
|
+
...entry.summary !== void 0 ? { summary: entry.summary } : {},
|
|
445
|
+
...entry.effect !== void 0 ? { effect: entry.effect } : {},
|
|
395
446
|
input: toCommandInput(entry.input)
|
|
396
447
|
});
|
|
397
448
|
}
|
|
@@ -498,6 +549,9 @@ var COMMAND_NAME = /^[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z][A-Za-z0-9]*)*$/;
|
|
|
498
549
|
var CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
499
550
|
var EFFECTS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
500
551
|
var PATH_BASE = "https://spfn.invalid";
|
|
552
|
+
function isSurfaceEffect(value) {
|
|
553
|
+
return EFFECTS.has(value);
|
|
554
|
+
}
|
|
501
555
|
function assertText(label, value) {
|
|
502
556
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
503
557
|
throw new SurfaceError(`${label} must be a non-empty string.`);
|
|
@@ -668,6 +722,8 @@ function surfaceRouteBuilder(namespace, wording) {
|
|
|
668
722
|
}
|
|
669
723
|
|
|
670
724
|
// src/surface/define-surface.ts
|
|
725
|
+
var MAX_COMMAND_SUMMARY_LENGTH = 500;
|
|
726
|
+
var EFFECT_METHOD = ".effect('read' | 'write' | 'destructive')";
|
|
671
727
|
function isRouter2(value) {
|
|
672
728
|
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
673
729
|
}
|
|
@@ -692,6 +748,43 @@ function assertSurfaceRoute(plan, name, def) {
|
|
|
692
748
|
);
|
|
693
749
|
}
|
|
694
750
|
}
|
|
751
|
+
function assertCommandMetadata(plan, name, def) {
|
|
752
|
+
const label = `${capitalized(plan.wording.noun)} command "${name}"`;
|
|
753
|
+
if (def.effect === void 0 && plan.requireEffect) {
|
|
754
|
+
throw new SurfaceError(
|
|
755
|
+
`${label} declares no effect. A client reads it to decide whether to call, retry, or ask for confirmation, so ${plan.wording.factory} refuses a command without one. Declare it on the route with ${EFFECT_METHOD}.`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (def.effect !== void 0 && !isSurfaceEffect(def.effect)) {
|
|
759
|
+
throw new SurfaceError(
|
|
760
|
+
`${label} has invalid effect ${JSON.stringify(def.effect)}. A client knows no other, so declare it with ${EFFECT_METHOD}.`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
assertCommandSummary(label, def.summary);
|
|
764
|
+
}
|
|
765
|
+
function assertCommandSummary(label, summary) {
|
|
766
|
+
if (summary === void 0) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (typeof summary !== "string" || summary.trim().length === 0) {
|
|
770
|
+
throw new SurfaceError(
|
|
771
|
+
`${label} has an empty summary. A client would list it as a command described by nothing; give .summary() one line of text, or leave it out.`
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (summary.length > MAX_COMMAND_SUMMARY_LENGTH) {
|
|
775
|
+
throw new SurfaceError(
|
|
776
|
+
`${label} has a summary of ${summary.length} characters, over the ${MAX_COMMAND_SUMMARY_LENGTH} a client accepts \u2014 it would drop the command's metadata and treat it as destructive. Shorten it.`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function assertModuleRouteEffect(plan, name, definition) {
|
|
781
|
+
const routeEffect = definition.route.effect;
|
|
782
|
+
if (routeEffect !== void 0 && routeEffect !== definition.effect) {
|
|
783
|
+
throw new SurfaceError(
|
|
784
|
+
`${capitalized(plan.wording.noun)} command "${name}" is declared ${JSON.stringify(definition.effect)} by its module, but its route says .effect(${JSON.stringify(routeEffect)}). The manifest announces the module's declaration, so the two cannot disagree. Drop the route's .effect() or make it match.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
695
788
|
function assertSurfaceName(plan, name) {
|
|
696
789
|
if (name === plan.manifestName) {
|
|
697
790
|
throw new SurfaceError(
|
|
@@ -699,7 +792,7 @@ function assertSurfaceName(plan, name) {
|
|
|
699
792
|
);
|
|
700
793
|
}
|
|
701
794
|
}
|
|
702
|
-
function rebuildNestedRouter(plan, name, router,
|
|
795
|
+
function rebuildNestedRouter(plan, name, router, commandsByName, inherited) {
|
|
703
796
|
if (router._packageRouters?.length > 0) {
|
|
704
797
|
throw new SurfaceError(
|
|
705
798
|
`${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.`
|
|
@@ -707,19 +800,19 @@ function rebuildNestedRouter(plan, name, router, guards, inherited) {
|
|
|
707
800
|
}
|
|
708
801
|
const handedDown = [...inherited, ...router._globalMiddlewares ?? []];
|
|
709
802
|
let rebuilt = defineUnmappedRouter(
|
|
710
|
-
secureRoutes(plan, router.routes,
|
|
803
|
+
secureRoutes(plan, router.routes, commandsByName, handedDown)
|
|
711
804
|
);
|
|
712
805
|
if (router._contractVersion) {
|
|
713
806
|
rebuilt = rebuilt.contractVersion(router._contractVersion);
|
|
714
807
|
}
|
|
715
808
|
return rebuilt;
|
|
716
809
|
}
|
|
717
|
-
function secureRoutes(plan, routes,
|
|
810
|
+
function secureRoutes(plan, routes, commandsByName, inherited = []) {
|
|
718
811
|
const secured = {};
|
|
719
812
|
for (const [name, entry] of Object.entries(routes)) {
|
|
720
813
|
assertSurfaceName(plan, name);
|
|
721
814
|
if (isRouter2(entry)) {
|
|
722
|
-
secured[name] = rebuildNestedRouter(plan, name, entry,
|
|
815
|
+
secured[name] = rebuildNestedRouter(plan, name, entry, commandsByName, inherited);
|
|
723
816
|
continue;
|
|
724
817
|
}
|
|
725
818
|
if (!isRouteDef2(entry)) {
|
|
@@ -728,9 +821,11 @@ function secureRoutes(plan, routes, guards, inherited = []) {
|
|
|
728
821
|
);
|
|
729
822
|
}
|
|
730
823
|
assertSurfaceRoute(plan, name, entry);
|
|
824
|
+
assertCommandMetadata(plan, name, entry);
|
|
825
|
+
const guard = plan.authorize?.(commandsByName.get(name));
|
|
731
826
|
secured[name] = {
|
|
732
827
|
...entry,
|
|
733
|
-
middlewares: commandChain(plan, name,
|
|
828
|
+
middlewares: commandChain(plan, name, guard, [...inherited, ...entry.middlewares ?? []])
|
|
734
829
|
};
|
|
735
830
|
}
|
|
736
831
|
return secured;
|
|
@@ -761,9 +856,6 @@ function refuseGuardCollision(plan, name, guardName, declared) {
|
|
|
761
856
|
function middlewareNameOf(middleware) {
|
|
762
857
|
return typeof middleware === "function" ? void 0 : middleware.name;
|
|
763
858
|
}
|
|
764
|
-
function guardsFor(plan, commands) {
|
|
765
|
-
return new Map(commands.map((command) => [command.name, plan.authorize?.(command)]));
|
|
766
|
-
}
|
|
767
859
|
function assertModuleCommandFree(plan, command, appCommands, claimed) {
|
|
768
860
|
const noun = plan.wording.noun;
|
|
769
861
|
if (claimed.names.has(command.name)) {
|
|
@@ -815,6 +907,7 @@ function compileModules(plan, appCommands) {
|
|
|
815
907
|
path: definition.route.path,
|
|
816
908
|
input: collectSurfaceCommands({ [localName]: definition.route }, plan.wording)[0].input
|
|
817
909
|
};
|
|
910
|
+
assertModuleRouteEffect(plan, command.name, definition);
|
|
818
911
|
assertModuleCommandFree(plan, command, appCommands, claimed);
|
|
819
912
|
compiled.commands.push(command);
|
|
820
913
|
compiled.routes[command.name] = { route: definition.route, command };
|
|
@@ -854,7 +947,8 @@ function buildRouter(plan, routes) {
|
|
|
854
947
|
...moduleSurface.descriptors.length > 0 ? { modules: moduleSurface.descriptors } : {},
|
|
855
948
|
commands
|
|
856
949
|
};
|
|
857
|
-
const
|
|
950
|
+
const commandsByName = new Map(appCommands.map((command) => [command.name, command]));
|
|
951
|
+
const secured = secureRoutes(plan, routes, commandsByName);
|
|
858
952
|
const securedModules = secureModuleRoutes(plan, moduleSurface.routes);
|
|
859
953
|
return Object.defineProperty(defineUnmappedRouter({
|
|
860
954
|
[plan.manifestName]: manifestRoute(plan, manifest),
|
|
@@ -862,7 +956,7 @@ function buildRouter(plan, routes) {
|
|
|
862
956
|
...securedModules
|
|
863
957
|
}), "_surfaceNamespace", { value: plan.namespace, enumerable: false });
|
|
864
958
|
}
|
|
865
|
-
function planOf(options, wording) {
|
|
959
|
+
function planOf(options, wording, rules) {
|
|
866
960
|
assertNamespace(options?.namespace);
|
|
867
961
|
if (!options.auth) {
|
|
868
962
|
throw new SurfaceError(
|
|
@@ -888,11 +982,12 @@ function planOf(options, wording) {
|
|
|
888
982
|
authorize: options.authorize,
|
|
889
983
|
manifest,
|
|
890
984
|
modules,
|
|
891
|
-
wording
|
|
985
|
+
wording,
|
|
986
|
+
requireEffect: rules.requireEffect
|
|
892
987
|
};
|
|
893
988
|
}
|
|
894
|
-
function createSurface(options, wording) {
|
|
895
|
-
const plan = planOf(options, wording);
|
|
989
|
+
function createSurface(options, wording, rules) {
|
|
990
|
+
const plan = planOf(options, wording, rules);
|
|
896
991
|
return {
|
|
897
992
|
namespace: plan.namespace,
|
|
898
993
|
route: surfaceRouteBuilder(plan.namespace, wording),
|
|
@@ -900,7 +995,7 @@ function createSurface(options, wording) {
|
|
|
900
995
|
};
|
|
901
996
|
}
|
|
902
997
|
function defineSurface(options) {
|
|
903
|
-
return createSurface(options, SURFACE_WORDING);
|
|
998
|
+
return createSurface(options, SURFACE_WORDING, { requireEffect: true });
|
|
904
999
|
}
|
|
905
1000
|
|
|
906
1001
|
export { SurfaceError, collectSurfaceCommands, defineSurface, defineSurfaceModule };
|