@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.
@@ -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-BHKgGkwI.js';
4
- import { R as Router } from '../router-DaDrprJd.js';
5
- import { c as SurfaceRouteBuilder, d as SurfaceCommand, b as SurfaceModule } from '../error-BcdBVfRA.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-BcdBVfRA.js';
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-D52SokSM.js';
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.
@@ -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, guards, inherited) {
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, guards, handedDown)
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, guards, inherited = []) {
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, guards, inherited);
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, guards.get(name), [...inherited, ...entry.middlewares ?? []])
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 secured = secureRoutes(plan, routes, guardsFor(plan, appCommands));
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 };