@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/README.md
CHANGED
|
@@ -43,7 +43,7 @@ Next.js owns the frontend, Drizzle owns the SQL, and your services own the rules
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
pnpm add @spfn/core@beta drizzle-orm@1.0.0-rc.4 postgres pg
|
|
46
|
-
# optional peer: next ^16.3.
|
|
46
|
+
# optional peer: next ^16.3.8 (only for the Next.js bridge)
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Node `>=20.0.0`. ESM only.
|
|
@@ -691,6 +691,10 @@ The manifest gains a `modules` array and per-command `module`, `summary`, `effec
|
|
|
691
691
|
`scopes`. All of it is additive — an app that mounts no modules serves exactly the v1
|
|
692
692
|
manifest it served before.
|
|
693
693
|
|
|
694
|
+
An app's own ops routes may declare `.effect()` and `.summary()` too, and the manifest
|
|
695
|
+
carries both for that command. On `/_ops` they are optional: a route declaring neither is
|
|
696
|
+
announced exactly as before.
|
|
697
|
+
|
|
694
698
|
```bash
|
|
695
699
|
spfn ops modules # what is mounted, and from where
|
|
696
700
|
spfn ops list --module ledger # just that module's commands
|
|
@@ -731,6 +735,8 @@ export const app = defineSurface({
|
|
|
731
735
|
|
|
732
736
|
export const pageAdd = app.route.post('/pages') // POST /_app/pages
|
|
733
737
|
.input({ body: Type.Object({ title: Type.String() }) })
|
|
738
|
+
.effect('write') // required: read | write | destructive
|
|
739
|
+
.summary('Add a page to the blueprint') // optional, at most 500 characters
|
|
734
740
|
.handler(async (c) => pagesRepository.add((await c.data()).body.title));
|
|
735
741
|
|
|
736
742
|
// src/server/router.ts — both surfaces, side by side
|
|
@@ -754,6 +760,12 @@ export const appRouter = defineRouter({ ... }).packages([app.router({ pageAdd })
|
|
|
754
760
|
module is mounted. A guard named like `auth` or like one of the command's group or own
|
|
755
761
|
middlewares is refused: registration keeps one middleware per name, so the other one would
|
|
756
762
|
never run.
|
|
763
|
+
- **Effect.** Every app command on a `defineSurface` surface declares `.effect()` on its
|
|
764
|
+
route; `router()` refuses one that does not, naming the command, so a client never
|
|
765
|
+
receives a command whose effect it cannot know. `/_ops` routes may declare it and need
|
|
766
|
+
not. The manifest announces `effect` and `summary` beside the command, and `authorize`
|
|
767
|
+
receives them on the command it is given. A module command's own declaration wins over its
|
|
768
|
+
route's: a route `.effect()` that disagrees with it is refused.
|
|
757
769
|
- **Modules.** A module is validated against the namespace of the surface that mounts it —
|
|
758
770
|
its commands must live under `<namespace>/<id>/`. Write one for a surface with
|
|
759
771
|
`defineSurfaceModule(module, '/_app')`; a module written for `/_ops` mounted on `/_app` is
|
package/dist/contract/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { E as EventRouterDef } from '../router-CysBmgv3.js';
|
|
2
|
-
import { R as Router } from '../router-
|
|
3
|
-
import { C as ContractDocument, a as ContractViolation, b as ContractOperation, c as ContractSnapshot } from '../types-
|
|
4
|
-
export { d as CompatibilityPolicy, e as ContractEvent, f as ContractEvents, g as ContractRequest, h as ContractViolationKind, J as JsonSchema } from '../types-
|
|
2
|
+
import { R as Router } from '../router-4YoIy5fN.js';
|
|
3
|
+
import { C as ContractDocument, a as ContractViolation, b as ContractOperation, c as ContractSnapshot } from '../types-5Rl11_Nb.js';
|
|
4
|
+
export { d as CompatibilityPolicy, e as ContractEvent, f as ContractEvents, g as ContractRequest, h as ContractViolationKind, J as JsonSchema } from '../types-5Rl11_Nb.js';
|
|
5
5
|
import '@sinclair/typebox';
|
|
6
6
|
import '../define-middleware-CVKgqo8S.js';
|
|
7
7
|
import 'hono';
|
|
8
|
-
import '../route-builder-
|
|
8
|
+
import '../route-builder-DRW-MSCJ.js';
|
|
9
9
|
import 'hono/utils/http-status';
|
|
10
10
|
import '../route/types.js';
|
|
11
11
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { R as RouteDef, f as RouteBuilder } from './route-builder-
|
|
2
|
-
import { J as JsonSchema } from './types-
|
|
3
|
-
import { R as Router } from './router-
|
|
4
|
-
import { HttpMethod } from './route/types.js';
|
|
1
|
+
import { R as RouteDef, f as RouteBuilder } from './route-builder-DRW-MSCJ.js';
|
|
2
|
+
import { J as JsonSchema } from './types-5Rl11_Nb.js';
|
|
3
|
+
import { R as Router } from './router-4YoIy5fN.js';
|
|
4
|
+
import { RouteEffect, HttpMethod } from './route/types.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* How refusals name the surface they come from.
|
|
@@ -35,7 +35,8 @@ interface SurfaceWording {
|
|
|
35
35
|
* than registered at paths outside the surface that is supposed to guard them.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** One vocabulary for module commands and an app's own: the route's `.effect()`. */
|
|
39
|
+
type SurfaceEffect = RouteEffect;
|
|
39
40
|
interface SurfaceModuleCommand<TRoute extends RouteDef<any> = RouteDef<any>> {
|
|
40
41
|
summary: string;
|
|
41
42
|
effect: SurfaceEffect;
|
|
@@ -115,8 +116,13 @@ interface SurfaceCommand {
|
|
|
115
116
|
path: string;
|
|
116
117
|
/** Present for commands contributed by an explicitly mounted module. */
|
|
117
118
|
module?: string;
|
|
119
|
+
/**
|
|
120
|
+
* A module command's from its declaration; an app command's from the
|
|
121
|
+
* route's `.summary()` and `.effect()`, present only when declared.
|
|
122
|
+
*/
|
|
118
123
|
summary?: string;
|
|
119
124
|
effect?: SurfaceEffect;
|
|
125
|
+
/** Present for module commands only. */
|
|
120
126
|
scopes?: string[];
|
|
121
127
|
/** Input sections the route declares, each as JSON Schema. */
|
|
122
128
|
input: {
|
|
@@ -134,8 +140,9 @@ interface SurfaceManifest {
|
|
|
134
140
|
}
|
|
135
141
|
/**
|
|
136
142
|
* Walk a routes record (nested routers included) and collect every RouteDef
|
|
137
|
-
* as a command
|
|
138
|
-
*
|
|
143
|
+
* as a command, with the summary and effect its route declared. Validation of
|
|
144
|
+
* paths and of that metadata happens in the surface's `router()`, so nothing
|
|
145
|
+
* here is checked but the uniqueness of names.
|
|
139
146
|
*/
|
|
140
147
|
declare function collectSurfaceCommands(routes: Record<string, RouteDef<any> | Router<any>>, wording?: SurfaceWording): SurfaceCommand[];
|
|
141
148
|
|
package/dist/ops/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 { S as SurfaceEffect, a as SurfaceModuleCommand, b as SurfaceModule, c as SurfaceRouteBuilder, d as SurfaceCommand, e as SurfaceManifest, f as SurfaceModuleDescriptor } from '../error-
|
|
6
|
-
export { g as OpsRouterError } from '../error-
|
|
3
|
+
import { R as RouteDef } from '../route-builder-DRW-MSCJ.js';
|
|
4
|
+
import { R as Router } from '../router-4YoIy5fN.js';
|
|
5
|
+
import { S as SurfaceEffect, a as SurfaceModuleCommand, b as SurfaceModule, c as SurfaceRouteBuilder, d as SurfaceCommand, e as SurfaceManifest, f as SurfaceModuleDescriptor } from '../error-TsUIjp_x.js';
|
|
6
|
+
export { g as OpsRouterError } 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
|
* Capability ops module contract
|
|
@@ -60,7 +60,10 @@ declare function defineOpsModule<TCommands extends Record<string, OpsModuleComma
|
|
|
60
60
|
* It is the command surface (`@spfn/core/surface`) fixed to `/_ops`, with the
|
|
61
61
|
* scope-factory `authorize` this factory has always taken adapted to the
|
|
62
62
|
* surface's per-command one: applied to module commands only, so every
|
|
63
|
-
* middleware chain is what it was before surfaces existed.
|
|
63
|
+
* middleware chain is what it was before surfaces existed. An ops route may
|
|
64
|
+
* declare `.effect()` and `.summary()` and the manifest carries them; unlike
|
|
65
|
+
* `defineSurface`, a route declaring neither is not refused, so an ops
|
|
66
|
+
* surface written before they existed boots and announces what it always did.
|
|
64
67
|
*
|
|
65
68
|
* @example
|
|
66
69
|
* ```ts
|
package/dist/ops/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.`);
|
|
@@ -665,6 +719,8 @@ function surfaceRouteBuilder(namespace, wording) {
|
|
|
665
719
|
}
|
|
666
720
|
|
|
667
721
|
// src/surface/define-surface.ts
|
|
722
|
+
var MAX_COMMAND_SUMMARY_LENGTH = 500;
|
|
723
|
+
var EFFECT_METHOD = ".effect('read' | 'write' | 'destructive')";
|
|
668
724
|
function isRouter2(value) {
|
|
669
725
|
return value !== null && typeof value === "object" && "routes" in value && "_routes" in value;
|
|
670
726
|
}
|
|
@@ -689,6 +745,43 @@ function assertSurfaceRoute(plan, name, def) {
|
|
|
689
745
|
);
|
|
690
746
|
}
|
|
691
747
|
}
|
|
748
|
+
function assertCommandMetadata(plan, name, def) {
|
|
749
|
+
const label = `${capitalized(plan.wording.noun)} command "${name}"`;
|
|
750
|
+
if (def.effect === void 0 && plan.requireEffect) {
|
|
751
|
+
throw new SurfaceError(
|
|
752
|
+
`${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}.`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (def.effect !== void 0 && !isSurfaceEffect(def.effect)) {
|
|
756
|
+
throw new SurfaceError(
|
|
757
|
+
`${label} has invalid effect ${JSON.stringify(def.effect)}. A client knows no other, so declare it with ${EFFECT_METHOD}.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
assertCommandSummary(label, def.summary);
|
|
761
|
+
}
|
|
762
|
+
function assertCommandSummary(label, summary) {
|
|
763
|
+
if (summary === void 0) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (typeof summary !== "string" || summary.trim().length === 0) {
|
|
767
|
+
throw new SurfaceError(
|
|
768
|
+
`${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.`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
if (summary.length > MAX_COMMAND_SUMMARY_LENGTH) {
|
|
772
|
+
throw new SurfaceError(
|
|
773
|
+
`${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.`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function assertModuleRouteEffect(plan, name, definition) {
|
|
778
|
+
const routeEffect = definition.route.effect;
|
|
779
|
+
if (routeEffect !== void 0 && routeEffect !== definition.effect) {
|
|
780
|
+
throw new SurfaceError(
|
|
781
|
+
`${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.`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
692
785
|
function assertSurfaceName(plan, name) {
|
|
693
786
|
if (name === plan.manifestName) {
|
|
694
787
|
throw new SurfaceError(
|
|
@@ -696,7 +789,7 @@ function assertSurfaceName(plan, name) {
|
|
|
696
789
|
);
|
|
697
790
|
}
|
|
698
791
|
}
|
|
699
|
-
function rebuildNestedRouter(plan, name, router,
|
|
792
|
+
function rebuildNestedRouter(plan, name, router, commandsByName, inherited) {
|
|
700
793
|
if (router._packageRouters?.length > 0) {
|
|
701
794
|
throw new SurfaceError(
|
|
702
795
|
`${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.`
|
|
@@ -704,19 +797,19 @@ function rebuildNestedRouter(plan, name, router, guards, inherited) {
|
|
|
704
797
|
}
|
|
705
798
|
const handedDown = [...inherited, ...router._globalMiddlewares ?? []];
|
|
706
799
|
let rebuilt = defineUnmappedRouter(
|
|
707
|
-
secureRoutes(plan, router.routes,
|
|
800
|
+
secureRoutes(plan, router.routes, commandsByName, handedDown)
|
|
708
801
|
);
|
|
709
802
|
if (router._contractVersion) {
|
|
710
803
|
rebuilt = rebuilt.contractVersion(router._contractVersion);
|
|
711
804
|
}
|
|
712
805
|
return rebuilt;
|
|
713
806
|
}
|
|
714
|
-
function secureRoutes(plan, routes,
|
|
807
|
+
function secureRoutes(plan, routes, commandsByName, inherited = []) {
|
|
715
808
|
const secured = {};
|
|
716
809
|
for (const [name, entry] of Object.entries(routes)) {
|
|
717
810
|
assertSurfaceName(plan, name);
|
|
718
811
|
if (isRouter2(entry)) {
|
|
719
|
-
secured[name] = rebuildNestedRouter(plan, name, entry,
|
|
812
|
+
secured[name] = rebuildNestedRouter(plan, name, entry, commandsByName, inherited);
|
|
720
813
|
continue;
|
|
721
814
|
}
|
|
722
815
|
if (!isRouteDef2(entry)) {
|
|
@@ -725,9 +818,11 @@ function secureRoutes(plan, routes, guards, inherited = []) {
|
|
|
725
818
|
);
|
|
726
819
|
}
|
|
727
820
|
assertSurfaceRoute(plan, name, entry);
|
|
821
|
+
assertCommandMetadata(plan, name, entry);
|
|
822
|
+
const guard = plan.authorize?.(commandsByName.get(name));
|
|
728
823
|
secured[name] = {
|
|
729
824
|
...entry,
|
|
730
|
-
middlewares: commandChain(plan, name,
|
|
825
|
+
middlewares: commandChain(plan, name, guard, [...inherited, ...entry.middlewares ?? []])
|
|
731
826
|
};
|
|
732
827
|
}
|
|
733
828
|
return secured;
|
|
@@ -758,9 +853,6 @@ function refuseGuardCollision(plan, name, guardName, declared) {
|
|
|
758
853
|
function middlewareNameOf(middleware) {
|
|
759
854
|
return typeof middleware === "function" ? void 0 : middleware.name;
|
|
760
855
|
}
|
|
761
|
-
function guardsFor(plan, commands) {
|
|
762
|
-
return new Map(commands.map((command) => [command.name, plan.authorize?.(command)]));
|
|
763
|
-
}
|
|
764
856
|
function assertModuleCommandFree(plan, command, appCommands, claimed) {
|
|
765
857
|
const noun = plan.wording.noun;
|
|
766
858
|
if (claimed.names.has(command.name)) {
|
|
@@ -812,6 +904,7 @@ function compileModules(plan, appCommands) {
|
|
|
812
904
|
path: definition.route.path,
|
|
813
905
|
input: collectSurfaceCommands({ [localName]: definition.route }, plan.wording)[0].input
|
|
814
906
|
};
|
|
907
|
+
assertModuleRouteEffect(plan, command.name, definition);
|
|
815
908
|
assertModuleCommandFree(plan, command, appCommands, claimed);
|
|
816
909
|
compiled.commands.push(command);
|
|
817
910
|
compiled.routes[command.name] = { route: definition.route, command };
|
|
@@ -851,7 +944,8 @@ function buildRouter(plan, routes) {
|
|
|
851
944
|
...moduleSurface.descriptors.length > 0 ? { modules: moduleSurface.descriptors } : {},
|
|
852
945
|
commands
|
|
853
946
|
};
|
|
854
|
-
const
|
|
947
|
+
const commandsByName = new Map(appCommands.map((command) => [command.name, command]));
|
|
948
|
+
const secured = secureRoutes(plan, routes, commandsByName);
|
|
855
949
|
const securedModules = secureModuleRoutes(plan, moduleSurface.routes);
|
|
856
950
|
return Object.defineProperty(defineUnmappedRouter({
|
|
857
951
|
[plan.manifestName]: manifestRoute(plan, manifest),
|
|
@@ -859,7 +953,7 @@ function buildRouter(plan, routes) {
|
|
|
859
953
|
...securedModules
|
|
860
954
|
}), "_surfaceNamespace", { value: plan.namespace, enumerable: false });
|
|
861
955
|
}
|
|
862
|
-
function planOf(options, wording) {
|
|
956
|
+
function planOf(options, wording, rules) {
|
|
863
957
|
assertNamespace(options?.namespace);
|
|
864
958
|
if (!options.auth) {
|
|
865
959
|
throw new SurfaceError(
|
|
@@ -885,11 +979,12 @@ function planOf(options, wording) {
|
|
|
885
979
|
authorize: options.authorize,
|
|
886
980
|
manifest,
|
|
887
981
|
modules,
|
|
888
|
-
wording
|
|
982
|
+
wording,
|
|
983
|
+
requireEffect: rules.requireEffect
|
|
889
984
|
};
|
|
890
985
|
}
|
|
891
|
-
function createSurface(options, wording) {
|
|
892
|
-
const plan = planOf(options, wording);
|
|
986
|
+
function createSurface(options, wording, rules) {
|
|
987
|
+
const plan = planOf(options, wording, rules);
|
|
893
988
|
return {
|
|
894
989
|
namespace: plan.namespace,
|
|
895
990
|
route: surfaceRouteBuilder(plan.namespace, wording),
|
|
@@ -928,7 +1023,7 @@ function createOpsRouter(routes, options) {
|
|
|
928
1023
|
auth: options?.auth,
|
|
929
1024
|
authorize: moduleScopeGuard(options?.authorize),
|
|
930
1025
|
modules: options?.modules
|
|
931
|
-
}, OPS_WORDING).router(routes);
|
|
1026
|
+
}, OPS_WORDING, { requireEffect: false }).router(routes);
|
|
932
1027
|
}
|
|
933
1028
|
|
|
934
1029
|
// src/ops/manifest.ts
|