@happyvertical/smrt-core 0.44.0 → 0.45.0

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.
Files changed (104) hide show
  1. package/AGENTS.md +8 -8
  2. package/agents/change-feed.md +3 -2
  3. package/agents/generators.md +34 -0
  4. package/agents/revision-guard.md +66 -0
  5. package/dist/browser.js +2 -1
  6. package/dist/cascade.d.ts +5 -9
  7. package/dist/cascade.d.ts.map +1 -1
  8. package/dist/cascade.js +65 -30
  9. package/dist/cascade.js.map +1 -1
  10. package/dist/change-feed.d.ts +60 -1
  11. package/dist/change-feed.d.ts.map +1 -1
  12. package/dist/change-feed.js +464 -27
  13. package/dist/change-feed.js.map +1 -1
  14. package/dist/change-signals.d.ts.map +1 -1
  15. package/dist/change-signals.js +13 -11
  16. package/dist/change-signals.js.map +1 -1
  17. package/dist/embedded-write-queue.d.ts +8 -0
  18. package/dist/embedded-write-queue.d.ts.map +1 -1
  19. package/dist/embedded-write-queue.js +12 -2
  20. package/dist/embedded-write-queue.js.map +1 -1
  21. package/dist/generators/cli.d.ts.map +1 -1
  22. package/dist/generators/cli.js +10 -18
  23. package/dist/generators/cli.js.map +1 -1
  24. package/dist/generators/custom-action.d.ts +216 -0
  25. package/dist/generators/custom-action.d.ts.map +1 -1
  26. package/dist/generators/custom-action.js +256 -1
  27. package/dist/generators/custom-action.js.map +1 -1
  28. package/dist/generators/index.d.ts +2 -1
  29. package/dist/generators/index.d.ts.map +1 -1
  30. package/dist/generators/index.js +3 -2
  31. package/dist/generators/mcp.d.ts.map +1 -1
  32. package/dist/generators/mcp.js +14 -39
  33. package/dist/generators/mcp.js.map +1 -1
  34. package/dist/generators/preflight-route.d.ts +151 -0
  35. package/dist/generators/preflight-route.d.ts.map +1 -0
  36. package/dist/generators/preflight-route.js +194 -0
  37. package/dist/generators/preflight-route.js.map +1 -0
  38. package/dist/generators/rest.d.ts +12 -0
  39. package/dist/generators/rest.d.ts.map +1 -1
  40. package/dist/generators/rest.js +14 -15
  41. package/dist/generators/rest.js.map +1 -1
  42. package/dist/generators/tool-schema.d.ts.map +1 -1
  43. package/dist/generators/tool-schema.js +2 -8
  44. package/dist/generators/tool-schema.js.map +1 -1
  45. package/dist/generators.js +3 -2
  46. package/dist/index.d.ts +3 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +7 -4
  49. package/dist/knowledge.d.ts.map +1 -1
  50. package/dist/knowledge.js +283 -35
  51. package/dist/knowledge.js.map +1 -1
  52. package/dist/manifest/static-manifest.d.ts.map +1 -1
  53. package/dist/manifest/static-manifest.js +6 -2
  54. package/dist/manifest/static-manifest.js.map +1 -1
  55. package/dist/manifest/store.js +1 -1
  56. package/dist/manifest/store.js.map +1 -1
  57. package/dist/manifest.json +8 -2
  58. package/dist/object.d.ts +31 -1
  59. package/dist/object.d.ts.map +1 -1
  60. package/dist/object.js +57 -9
  61. package/dist/object.js.map +1 -1
  62. package/dist/registry/framework-base-classes.d.ts +10 -0
  63. package/dist/registry/framework-base-classes.d.ts.map +1 -0
  64. package/dist/registry/framework-base-classes.js +92 -0
  65. package/dist/registry/framework-base-classes.js.map +1 -0
  66. package/dist/registry/schema-builder.d.ts.map +1 -1
  67. package/dist/registry/schema-builder.js +2 -0
  68. package/dist/registry/schema-builder.js.map +1 -1
  69. package/dist/registry/types.d.ts +27 -6
  70. package/dist/registry/types.d.ts.map +1 -1
  71. package/dist/registry.d.ts +1 -0
  72. package/dist/registry.d.ts.map +1 -1
  73. package/dist/registry.js +2 -1
  74. package/dist/registry.js.map +1 -1
  75. package/dist/revision-guard.d.ts +84 -0
  76. package/dist/revision-guard.d.ts.map +1 -0
  77. package/dist/revision-guard.js +120 -0
  78. package/dist/revision-guard.js.map +1 -0
  79. package/dist/scanner/manifest-generator.d.ts +46 -16
  80. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  81. package/dist/scanner/manifest-generator.js +198 -45
  82. package/dist/scanner/manifest-generator.js.map +1 -1
  83. package/dist/smrt-knowledge.json +18 -9
  84. package/dist/system/bootstrap.d.ts.map +1 -1
  85. package/dist/system/bootstrap.js +2 -1
  86. package/dist/system/bootstrap.js.map +1 -1
  87. package/dist/system/schema.d.ts +75 -2
  88. package/dist/system/schema.d.ts.map +1 -1
  89. package/dist/system/schema.js +326 -4
  90. package/dist/system/schema.js.map +1 -1
  91. package/dist/vite-plugin/index.d.ts.map +1 -1
  92. package/dist/vite-plugin/index.js +48 -23
  93. package/dist/vite-plugin/index.js.map +1 -1
  94. package/dist/vite-plugin/sveltekit-generator.d.ts +33 -7
  95. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  96. package/dist/vite-plugin/sveltekit-generator.js +88 -17
  97. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  98. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  99. package/dist/vite-plugin/sync-apply-route.js +2 -0
  100. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  101. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  102. package/dist/vite-plugin/web-collections.js +28 -5
  103. package/dist/vite-plugin/web-collections.js.map +1 -1
  104. package/package.json +4 -4
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Generated `_preflight` HTTP route for browser-plane playbook preflight
3
+ * (issue #2590).
4
+ *
5
+ * Handles `GET {basePath}/_preflight?key=<playbook key>` in the REST generator:
6
+ * an advisory, read-effect, idempotent report of what a caller's playbook would
7
+ * be allowed to do — *predicted*, never granted. Every step is authorized again
8
+ * where it executes; this route is capability **selection**, never
9
+ * authorization (epic #2585 invariant 2).
10
+ *
11
+ * ## `authMiddleware` is never invoked here
12
+ *
13
+ * Generated REST authorization is
14
+ * `authMiddleware?: (objectName, action) => (req) => Promise<Request | Response>`
15
+ * — request-bound, `Response`-returning rather than boolean, and free to consult
16
+ * session stores, rate-limit, or audit. It is not a dry-run predicate, and a
17
+ * synthetic-`Request` dry run of it would be a side effect the caller never
18
+ * asked for.
19
+ *
20
+ * That is enforced **structurally**, not by discipline:
21
+ * {@link PlaybookPreflightRouteOptions} has no `authMiddleware` member and no
22
+ * function-valued auth member of any kind. `rest.ts` passes the boolean
23
+ * `appAuthConfigured` and nothing else, so there is no handle in this module to
24
+ * invoke even by mistake. The app-auth layer is consequently reported as
25
+ * `unknown`, which is the honest answer.
26
+ *
27
+ * ## Why the provider is injected
28
+ *
29
+ * Resolution and verdict shaping live in `@happyvertical/smrt-playbooks`, which
30
+ * depends on this package. Core therefore owns the route and the static-layer
31
+ * facts (`ObjectRegistry` is core's), and takes the evaluator as a seam — the
32
+ * dependency stays one-way.
33
+ */
34
+ /** Path segment the preflight route is served at, under the API base path. */
35
+ export declare const PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT = "_preflight";
36
+ /**
37
+ * Capability classification of the preflight route: a read, safely repeatable,
38
+ * and closed-world. It is admitted by a default read-only browser exposure
39
+ * policy without an opt-in, because it reports on the system rather than
40
+ * changing it.
41
+ */
42
+ export declare const PLAYBOOK_PREFLIGHT_CAPABILITY: Readonly<{
43
+ readonly effect: "read";
44
+ readonly idempotent: true;
45
+ readonly openWorld: false;
46
+ }>;
47
+ /**
48
+ * The request handed to a preflight provider. Deliberately carries no request,
49
+ * no headers, and no auth handle — only the caller-scoped facts the static
50
+ * layers need.
51
+ */
52
+ export interface PlaybookPreflightRouteRequest {
53
+ /** The requested playbook key. */
54
+ key: string;
55
+ /** Always `'browser'` from this route. */
56
+ plane: 'browser';
57
+ /** The caller's published permission slugs, when the API context has them. */
58
+ permissions?: Iterable<string>;
59
+ /** Whether an app auth middleware is wired. A boolean; never the function. */
60
+ appAuthConfigured: boolean;
61
+ /** The generator's `APIContext.db`. */
62
+ db?: unknown;
63
+ }
64
+ /**
65
+ * Host-supplied browser-plane preflight evaluator, wired from
66
+ * `@happyvertical/smrt-playbooks`. Returns a JSON-serializable report.
67
+ */
68
+ export type PlaybookPreflightProvider = (request: PlaybookPreflightRouteRequest) => Promise<unknown>;
69
+ /**
70
+ * Options for {@link handlePlaybookPreflightRoute}.
71
+ *
72
+ * There is intentionally no `authMiddleware` member — see the module docs.
73
+ */
74
+ export interface PlaybookPreflightRouteOptions {
75
+ provider?: PlaybookPreflightProvider;
76
+ permissions?: Iterable<string>;
77
+ appAuthConfigured: boolean;
78
+ db?: unknown;
79
+ }
80
+ /**
81
+ * Serves `GET {basePath}/_preflight?key=<key>`.
82
+ *
83
+ * Unresolvable keys are the provider's concern: it returns the same uniform
84
+ * "unavailable" body for an unknown key as for an unauthorized one, and this
85
+ * route serves whatever it returns with an unconditional 200, so the HTTP layer
86
+ * adds no way to tell them apart either.
87
+ */
88
+ export declare function handlePlaybookPreflightRoute(req: Request, options: PlaybookPreflightRouteOptions): Promise<Response>;
89
+ /**
90
+ * Resolves a qualified model reference (`@happyvertical/smrt-commerce:Order`)
91
+ * to the name the registry and the REST generator key configuration by.
92
+ *
93
+ * Returns `undefined` for anything this build does not register, so the caller
94
+ * fails closed rather than reporting on a model that has no route.
95
+ */
96
+ export declare function resolveRegisteredObjectName(model: string): string | undefined;
97
+ /**
98
+ * The HTTP method a REST action is served by.
99
+ *
100
+ * CRUD actions map to their fixed verbs. A custom action is served under the
101
+ * method its own route config declares (`api.routes[action].method`, defaulting
102
+ * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing
103
+ * `POST` for a declared `GET` action would make preflight report a false `deny`
104
+ * on a `public: 'read'` model — hiding a playbook the caller can actually run,
105
+ * which is the tool-listing case this exists to serve. Without an `objectName`
106
+ * to read the declaration from, the fail-closed `POST` remains.
107
+ */
108
+ export declare function restMethodForApiAction(action: string, objectName?: string): string;
109
+ /**
110
+ * Whether `@smrt({ api })` exposes `action` on `objectName`.
111
+ *
112
+ * The generator's own action gate, lifted to a module function so preflight and
113
+ * the live route read the same rule from one place. Accepts any action name so
114
+ * a custom action is gated by the same `include`/`exclude` lists.
115
+ *
116
+ * @remarks An absent `objectName` returns `true` — fail-**open**, because the
117
+ * live route reaches this only after it has already resolved a model, and an
118
+ * unnamed object there means "not gated by this rule". A caller *predicting*
119
+ * rather than serving has no such guarantee and must reject an unresolvable
120
+ * model before asking (see `createRestPreflightLayerSource`).
121
+ */
122
+ export declare function isApiActionEnabledForObject(objectName: string | undefined, action: string): boolean;
123
+ /**
124
+ * Whether `action` names a route the generated REST surface can actually
125
+ * dispatch on `objectName`.
126
+ *
127
+ * CRUD actions always have a route. A custom action exists only when the
128
+ * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`
129
+ * iterates exactly that map, so an action absent from it can never execute no
130
+ * matter what `include`/`exclude` say. Exposure alone would report a typo'd or
131
+ * removed custom action as `allow`, and an agent would then start the earlier
132
+ * steps of a non-atomic playbook before dying on it — the failure preflight
133
+ * exists to prevent.
134
+ */
135
+ export declare function isRestActionRoutable(objectName: string | undefined, action: string): boolean;
136
+ /**
137
+ * Fail-closed authorization posture (#1540): true only when the object opts out
138
+ * of auth via `@smrt({ api: { public } })` — `true` for every method, `'read'`
139
+ * for safe (GET) methods only.
140
+ */
141
+ export declare function isRestRoutePublic(objectName: string | undefined, method: string): boolean;
142
+ /**
143
+ * Field-level read-permission slugs declared by `objectName`.
144
+ *
145
+ * Reads the model's own and inherited fields only — deliberately **not** the
146
+ * STI base-plus-descendants union used for cache policy. That union answers
147
+ * "does any variant carry a read-permission field?"; used here it would report
148
+ * a sibling variant's slugs as required for a step naming this concrete model.
149
+ */
150
+ export declare function restFieldReadPermissions(objectName: string | undefined): string[];
151
+ //# sourceMappingURL=preflight-route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight-route.d.ts","sourceRoot":"","sources":["../../src/generators/preflight-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAKH,8EAA8E;AAC9E,eAAO,MAAM,gCAAgC,eAAe,CAAC;AAE7D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B;;;;EAI/B,CAAC;AAEZ;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,0CAA0C;IAC1C,KAAK,EAAE,SAAS,CAAC;IACjB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,8EAA8E;IAC9E,iBAAiB,EAAE,OAAO,CAAC;IAC3B,uCAAuC;IACvC,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,CACtC,OAAO,EAAE,6BAA6B,KACnC,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IACrC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC/B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,EAAE,CAAC,EAAE,OAAO,CAAC;CACd;AAYD;;;;;;;GAOG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,QAAQ,CAAC,CAwBnB;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAK7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CA4BR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAsBT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAkBT;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,MAAM,EAAE,CAiBV"}
@@ -0,0 +1,194 @@
1
+ import { ObjectRegistry } from "../registry.js";
2
+ import { PRIVATE_READ_CACHE_CONTROL } from "./conditional-get.js";
3
+ //#region src/generators/preflight-route.ts
4
+ /**
5
+ * Generated `_preflight` HTTP route for browser-plane playbook preflight
6
+ * (issue #2590).
7
+ *
8
+ * Handles `GET {basePath}/_preflight?key=<playbook key>` in the REST generator:
9
+ * an advisory, read-effect, idempotent report of what a caller's playbook would
10
+ * be allowed to do — *predicted*, never granted. Every step is authorized again
11
+ * where it executes; this route is capability **selection**, never
12
+ * authorization (epic #2585 invariant 2).
13
+ *
14
+ * ## `authMiddleware` is never invoked here
15
+ *
16
+ * Generated REST authorization is
17
+ * `authMiddleware?: (objectName, action) => (req) => Promise<Request | Response>`
18
+ * — request-bound, `Response`-returning rather than boolean, and free to consult
19
+ * session stores, rate-limit, or audit. It is not a dry-run predicate, and a
20
+ * synthetic-`Request` dry run of it would be a side effect the caller never
21
+ * asked for.
22
+ *
23
+ * That is enforced **structurally**, not by discipline:
24
+ * {@link PlaybookPreflightRouteOptions} has no `authMiddleware` member and no
25
+ * function-valued auth member of any kind. `rest.ts` passes the boolean
26
+ * `appAuthConfigured` and nothing else, so there is no handle in this module to
27
+ * invoke even by mistake. The app-auth layer is consequently reported as
28
+ * `unknown`, which is the honest answer.
29
+ *
30
+ * ## Why the provider is injected
31
+ *
32
+ * Resolution and verdict shaping live in `@happyvertical/smrt-playbooks`, which
33
+ * depends on this package. Core therefore owns the route and the static-layer
34
+ * facts (`ObjectRegistry` is core's), and takes the evaluator as a seam — the
35
+ * dependency stays one-way.
36
+ */
37
+ /** Path segment the preflight route is served at, under the API base path. */
38
+ var PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT = "_preflight";
39
+ /**
40
+ * Capability classification of the preflight route: a read, safely repeatable,
41
+ * and closed-world. It is admitted by a default read-only browser exposure
42
+ * policy without an opt-in, because it reports on the system rather than
43
+ * changing it.
44
+ */
45
+ var PLAYBOOK_PREFLIGHT_CAPABILITY = Object.freeze({
46
+ effect: "read",
47
+ idempotent: true,
48
+ openWorld: false
49
+ });
50
+ function jsonResponse(body, status) {
51
+ return new Response(JSON.stringify(body), {
52
+ status,
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ "Cache-Control": PRIVATE_READ_CACHE_CONTROL
56
+ }
57
+ });
58
+ }
59
+ /**
60
+ * Serves `GET {basePath}/_preflight?key=<key>`.
61
+ *
62
+ * Unresolvable keys are the provider's concern: it returns the same uniform
63
+ * "unavailable" body for an unknown key as for an unauthorized one, and this
64
+ * route serves whatever it returns with an unconditional 200, so the HTTP layer
65
+ * adds no way to tell them apart either.
66
+ */
67
+ async function handlePlaybookPreflightRoute(req, options) {
68
+ if (!options.provider) return jsonResponse({ error: "Not found" }, 404);
69
+ if (req.method !== "GET") return jsonResponse({ error: "Method not allowed" }, 405);
70
+ const key = new URL(req.url).searchParams.get("key");
71
+ if (!key) return jsonResponse({ error: "Query parameter 'key' is required" }, 400);
72
+ return jsonResponse(await options.provider({
73
+ key,
74
+ plane: "browser",
75
+ ...options.permissions ? { permissions: options.permissions } : {},
76
+ appAuthConfigured: options.appAuthConfigured,
77
+ db: options.db
78
+ }), 200);
79
+ }
80
+ /**
81
+ * Resolves a qualified model reference (`@happyvertical/smrt-commerce:Order`)
82
+ * to the name the registry and the REST generator key configuration by.
83
+ *
84
+ * Returns `undefined` for anything this build does not register, so the caller
85
+ * fails closed rather than reporting on a model that has no route.
86
+ */
87
+ function resolveRegisteredObjectName(model) {
88
+ return (model.includes(":") ? ObjectRegistry.getClassByQualifiedName(model) : ObjectRegistry.getClass(model))?.name;
89
+ }
90
+ /**
91
+ * The HTTP method a REST action is served by.
92
+ *
93
+ * CRUD actions map to their fixed verbs. A custom action is served under the
94
+ * method its own route config declares (`api.routes[action].method`, defaulting
95
+ * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing
96
+ * `POST` for a declared `GET` action would make preflight report a false `deny`
97
+ * on a `public: 'read'` model — hiding a playbook the caller can actually run,
98
+ * which is the tool-listing case this exists to serve. Without an `objectName`
99
+ * to read the declaration from, the fail-closed `POST` remains.
100
+ */
101
+ function restMethodForApiAction(action, objectName) {
102
+ switch (action) {
103
+ case "list":
104
+ case "get": return "GET";
105
+ case "create": return "POST";
106
+ case "update": return "PUT";
107
+ case "delete": return "DELETE";
108
+ default: break;
109
+ }
110
+ if (!objectName) return "POST";
111
+ const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
112
+ if (!apiConfig || typeof apiConfig !== "object" || !apiConfig.routes) return "POST";
113
+ return (apiConfig.routes[action]?.method ?? "POST").toUpperCase();
114
+ }
115
+ /**
116
+ * Whether `@smrt({ api })` exposes `action` on `objectName`.
117
+ *
118
+ * The generator's own action gate, lifted to a module function so preflight and
119
+ * the live route read the same rule from one place. Accepts any action name so
120
+ * a custom action is gated by the same `include`/`exclude` lists.
121
+ *
122
+ * @remarks An absent `objectName` returns `true` — fail-**open**, because the
123
+ * live route reaches this only after it has already resolved a model, and an
124
+ * unnamed object there means "not gated by this rule". A caller *predicting*
125
+ * rather than serving has no such guarantee and must reject an unresolvable
126
+ * model before asking (see `createRestPreflightLayerSource`).
127
+ */
128
+ function isApiActionEnabledForObject(objectName, action) {
129
+ if (!objectName) return true;
130
+ const apiConfig = ObjectRegistry.getConfig(objectName).api;
131
+ if (apiConfig === false) return false;
132
+ if (apiConfig && typeof apiConfig === "object") {
133
+ if (apiConfig.include && !apiConfig.include.includes(action)) return false;
134
+ if (apiConfig.exclude?.includes(action)) return false;
135
+ }
136
+ return true;
137
+ }
138
+ /**
139
+ * Whether `action` names a route the generated REST surface can actually
140
+ * dispatch on `objectName`.
141
+ *
142
+ * CRUD actions always have a route. A custom action exists only when the
143
+ * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`
144
+ * iterates exactly that map, so an action absent from it can never execute no
145
+ * matter what `include`/`exclude` say. Exposure alone would report a typo'd or
146
+ * removed custom action as `allow`, and an agent would then start the earlier
147
+ * steps of a non-atomic playbook before dying on it — the failure preflight
148
+ * exists to prevent.
149
+ */
150
+ function isRestActionRoutable(objectName, action) {
151
+ if (!objectName) return false;
152
+ if (action === "list" || action === "get" || action === "create" || action === "update" || action === "delete") return true;
153
+ const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
154
+ if (!apiConfig || typeof apiConfig !== "object" || !apiConfig.routes) return false;
155
+ return Object.hasOwn(apiConfig.routes, action);
156
+ }
157
+ /**
158
+ * Fail-closed authorization posture (#1540): true only when the object opts out
159
+ * of auth via `@smrt({ api: { public } })` — `true` for every method, `'read'`
160
+ * for safe (GET) methods only.
161
+ */
162
+ function isRestRoutePublic(objectName, method) {
163
+ if (!objectName) return false;
164
+ const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
165
+ if (!apiConfig || typeof apiConfig !== "object") return false;
166
+ const publicAccess = apiConfig.public;
167
+ if (publicAccess === true) return true;
168
+ if (publicAccess === "read") return method.toUpperCase() === "GET";
169
+ return false;
170
+ }
171
+ /**
172
+ * Field-level read-permission slugs declared by `objectName`.
173
+ *
174
+ * Reads the model's own and inherited fields only — deliberately **not** the
175
+ * STI base-plus-descendants union used for cache policy. That union answers
176
+ * "does any variant carry a read-permission field?"; used here it would report
177
+ * a sibling variant's slugs as required for a step naming this concrete model.
178
+ */
179
+ function restFieldReadPermissions(objectName) {
180
+ if (!objectName) return [];
181
+ const registered = ObjectRegistry.getClass(objectName);
182
+ const className = registered?.qualifiedName ?? registered?.name ?? objectName;
183
+ const fields = registered?.inheritedFields ?? ObjectRegistry.getFields(className);
184
+ const slugs = /* @__PURE__ */ new Set();
185
+ for (const [, def] of fields) {
186
+ const slug = typeof def?.readPermission === "string" ? def.readPermission : typeof def?._meta?.readPermission === "string" ? def._meta.readPermission : void 0;
187
+ if (slug) slugs.add(slug);
188
+ }
189
+ return [...slugs].sort();
190
+ }
191
+ //#endregion
192
+ export { PLAYBOOK_PREFLIGHT_CAPABILITY, PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT, handlePlaybookPreflightRoute, isApiActionEnabledForObject, isRestActionRoutable, isRestRoutePublic, resolveRegisteredObjectName, restFieldReadPermissions, restMethodForApiAction };
193
+
194
+ //# sourceMappingURL=preflight-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preflight-route.js","names":[],"sources":["../../src/generators/preflight-route.ts"],"sourcesContent":["/**\n * Generated `_preflight` HTTP route for browser-plane playbook preflight\n * (issue #2590).\n *\n * Handles `GET {basePath}/_preflight?key=<playbook key>` in the REST generator:\n * an advisory, read-effect, idempotent report of what a caller's playbook would\n * be allowed to do — *predicted*, never granted. Every step is authorized again\n * where it executes; this route is capability **selection**, never\n * authorization (epic #2585 invariant 2).\n *\n * ## `authMiddleware` is never invoked here\n *\n * Generated REST authorization is\n * `authMiddleware?: (objectName, action) => (req) => Promise<Request | Response>`\n * — request-bound, `Response`-returning rather than boolean, and free to consult\n * session stores, rate-limit, or audit. It is not a dry-run predicate, and a\n * synthetic-`Request` dry run of it would be a side effect the caller never\n * asked for.\n *\n * That is enforced **structurally**, not by discipline:\n * {@link PlaybookPreflightRouteOptions} has no `authMiddleware` member and no\n * function-valued auth member of any kind. `rest.ts` passes the boolean\n * `appAuthConfigured` and nothing else, so there is no handle in this module to\n * invoke even by mistake. The app-auth layer is consequently reported as\n * `unknown`, which is the honest answer.\n *\n * ## Why the provider is injected\n *\n * Resolution and verdict shaping live in `@happyvertical/smrt-playbooks`, which\n * depends on this package. Core therefore owns the route and the static-layer\n * facts (`ObjectRegistry` is core's), and takes the evaluator as a seam — the\n * dependency stays one-way.\n */\n\nimport { ObjectRegistry } from '../registry.js';\nimport { PRIVATE_READ_CACHE_CONTROL } from './conditional-get.js';\n\n/** Path segment the preflight route is served at, under the API base path. */\nexport const PLAYBOOK_PREFLIGHT_ROUTE_SEGMENT = '_preflight';\n\n/**\n * Capability classification of the preflight route: a read, safely repeatable,\n * and closed-world. It is admitted by a default read-only browser exposure\n * policy without an opt-in, because it reports on the system rather than\n * changing it.\n */\nexport const PLAYBOOK_PREFLIGHT_CAPABILITY = Object.freeze({\n effect: 'read',\n idempotent: true,\n openWorld: false,\n} as const);\n\n/**\n * The request handed to a preflight provider. Deliberately carries no request,\n * no headers, and no auth handle — only the caller-scoped facts the static\n * layers need.\n */\nexport interface PlaybookPreflightRouteRequest {\n /** The requested playbook key. */\n key: string;\n /** Always `'browser'` from this route. */\n plane: 'browser';\n /** The caller's published permission slugs, when the API context has them. */\n permissions?: Iterable<string>;\n /** Whether an app auth middleware is wired. A boolean; never the function. */\n appAuthConfigured: boolean;\n /** The generator's `APIContext.db`. */\n db?: unknown;\n}\n\n/**\n * Host-supplied browser-plane preflight evaluator, wired from\n * `@happyvertical/smrt-playbooks`. Returns a JSON-serializable report.\n */\nexport type PlaybookPreflightProvider = (\n request: PlaybookPreflightRouteRequest,\n) => Promise<unknown>;\n\n/**\n * Options for {@link handlePlaybookPreflightRoute}.\n *\n * There is intentionally no `authMiddleware` member — see the module docs.\n */\nexport interface PlaybookPreflightRouteOptions {\n provider?: PlaybookPreflightProvider;\n permissions?: Iterable<string>;\n appAuthConfigured: boolean;\n db?: unknown;\n}\n\nfunction jsonResponse(body: unknown, status: number): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: {\n 'Content-Type': 'application/json',\n 'Cache-Control': PRIVATE_READ_CACHE_CONTROL,\n },\n });\n}\n\n/**\n * Serves `GET {basePath}/_preflight?key=<key>`.\n *\n * Unresolvable keys are the provider's concern: it returns the same uniform\n * \"unavailable\" body for an unknown key as for an unauthorized one, and this\n * route serves whatever it returns with an unconditional 200, so the HTTP layer\n * adds no way to tell them apart either.\n */\nexport async function handlePlaybookPreflightRoute(\n req: Request,\n options: PlaybookPreflightRouteOptions,\n): Promise<Response> {\n if (!options.provider) {\n // Route not wired for this deployment. Says nothing about any key.\n return jsonResponse({ error: 'Not found' }, 404);\n }\n\n if (req.method !== 'GET') {\n return jsonResponse({ error: 'Method not allowed' }, 405);\n }\n\n const key = new URL(req.url).searchParams.get('key');\n if (!key) {\n return jsonResponse({ error: \"Query parameter 'key' is required\" }, 400);\n }\n\n const report = await options.provider({\n key,\n plane: 'browser',\n ...(options.permissions ? { permissions: options.permissions } : {}),\n appAuthConfigured: options.appAuthConfigured,\n db: options.db,\n });\n\n return jsonResponse(report, 200);\n}\n\n/**\n * Resolves a qualified model reference (`@happyvertical/smrt-commerce:Order`)\n * to the name the registry and the REST generator key configuration by.\n *\n * Returns `undefined` for anything this build does not register, so the caller\n * fails closed rather than reporting on a model that has no route.\n */\nexport function resolveRegisteredObjectName(model: string): string | undefined {\n const registered = model.includes(':')\n ? ObjectRegistry.getClassByQualifiedName(model)\n : ObjectRegistry.getClass(model);\n return registered?.name;\n}\n\n/**\n * The HTTP method a REST action is served by.\n *\n * CRUD actions map to their fixed verbs. A custom action is served under the\n * method its own route config declares (`api.routes[action].method`, defaulting\n * to `POST` exactly as `dispatchCustomCollectionAction` does), because guessing\n * `POST` for a declared `GET` action would make preflight report a false `deny`\n * on a `public: 'read'` model — hiding a playbook the caller can actually run,\n * which is the tool-listing case this exists to serve. Without an `objectName`\n * to read the declaration from, the fail-closed `POST` remains.\n */\nexport function restMethodForApiAction(\n action: string,\n objectName?: string,\n): string {\n switch (action) {\n case 'list':\n case 'get':\n return 'GET';\n case 'create':\n return 'POST';\n case 'update':\n return 'PUT';\n case 'delete':\n return 'DELETE';\n default:\n break;\n }\n\n if (!objectName) {\n return 'POST';\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object' || !apiConfig.routes) {\n return 'POST';\n }\n\n const route = (apiConfig.routes as Record<string, { method?: string }>)[\n action\n ];\n return (route?.method ?? 'POST').toUpperCase();\n}\n\n/**\n * Whether `@smrt({ api })` exposes `action` on `objectName`.\n *\n * The generator's own action gate, lifted to a module function so preflight and\n * the live route read the same rule from one place. Accepts any action name so\n * a custom action is gated by the same `include`/`exclude` lists.\n *\n * @remarks An absent `objectName` returns `true` — fail-**open**, because the\n * live route reaches this only after it has already resolved a model, and an\n * unnamed object there means \"not gated by this rule\". A caller *predicting*\n * rather than serving has no such guarantee and must reject an unresolvable\n * model before asking (see `createRestPreflightLayerSource`).\n */\nexport function isApiActionEnabledForObject(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName).api;\n\n if (apiConfig === false) {\n return false;\n }\n\n if (apiConfig && typeof apiConfig === 'object') {\n if (apiConfig.include && !apiConfig.include.includes(action)) {\n return false;\n }\n\n if (apiConfig.exclude?.includes(action)) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Whether `action` names a route the generated REST surface can actually\n * dispatch on `objectName`.\n *\n * CRUD actions always have a route. A custom action exists only when the\n * decorator declares it in `api.routes` — `dispatchCustomCollectionAction`\n * iterates exactly that map, so an action absent from it can never execute no\n * matter what `include`/`exclude` say. Exposure alone would report a typo'd or\n * removed custom action as `allow`, and an agent would then start the earlier\n * steps of a non-atomic playbook before dying on it — the failure preflight\n * exists to prevent.\n */\nexport function isRestActionRoutable(\n objectName: string | undefined,\n action: string,\n): boolean {\n if (!objectName) return false;\n if (\n action === 'list' ||\n action === 'get' ||\n action === 'create' ||\n action === 'update' ||\n action === 'delete'\n ) {\n return true;\n }\n\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object' || !apiConfig.routes) {\n return false;\n }\n\n return Object.hasOwn(apiConfig.routes as Record<string, unknown>, action);\n}\n\n/**\n * Fail-closed authorization posture (#1540): true only when the object opts out\n * of auth via `@smrt({ api: { public } })` — `true` for every method, `'read'`\n * for safe (GET) methods only.\n */\nexport function isRestRoutePublic(\n objectName: string | undefined,\n method: string,\n): boolean {\n if (!objectName) return false;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api;\n if (!apiConfig || typeof apiConfig !== 'object') return false;\n const publicAccess = (apiConfig as { public?: boolean | 'read' }).public;\n if (publicAccess === true) return true;\n if (publicAccess === 'read') return method.toUpperCase() === 'GET';\n return false;\n}\n\n/**\n * Field-level read-permission slugs declared by `objectName`.\n *\n * Reads the model's own and inherited fields only — deliberately **not** the\n * STI base-plus-descendants union used for cache policy. That union answers\n * \"does any variant carry a read-permission field?\"; used here it would report\n * a sibling variant's slugs as required for a step naming this concrete model.\n */\nexport function restFieldReadPermissions(\n objectName: string | undefined,\n): string[] {\n if (!objectName) return [];\n const registered = ObjectRegistry.getClass(objectName);\n const className = registered?.qualifiedName ?? registered?.name ?? objectName;\n const fields =\n registered?.inheritedFields ?? ObjectRegistry.getFields(className);\n const slugs = new Set<string>();\n for (const [, def] of fields) {\n const slug =\n typeof def?.readPermission === 'string'\n ? def.readPermission\n : typeof def?._meta?.readPermission === 'string'\n ? (def._meta.readPermission as string)\n : undefined;\n if (slug) slugs.add(slug);\n }\n return [...slugs].sort();\n}\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { default as http } from 'node:http';
2
2
  import { SmrtCollection } from '../collection';
3
3
  import { SmrtObject } from '../object';
4
4
  import { RegisteredClass } from '../registry/types';
5
+ import { PlaybookPreflightProvider } from './preflight-route';
5
6
  export interface APIConfig {
6
7
  basePath?: string;
7
8
  enableCors?: boolean;
@@ -46,6 +47,17 @@ export interface APIConfig {
46
47
  * through this runtime server.
47
48
  */
48
49
  manifestHash?: string;
50
+ /**
51
+ * Browser-plane playbook preflight provider (#2590), wired from
52
+ * `@happyvertical/smrt-playbooks`. When omitted the `_preflight` route is not
53
+ * served at all.
54
+ *
55
+ * The provider receives only the requested key, the caller's published
56
+ * permission slugs, the database handle, and the boolean
57
+ * `appAuthConfigured`. It never receives {@link APIConfig.authMiddleware},
58
+ * which preflight must never invoke.
59
+ */
60
+ playbookPreflight?: PlaybookPreflightProvider;
49
61
  }
50
62
  export interface APIContext {
51
63
  db?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,13 @@ import { isTenantScopedClassResolved, resolveDispatchTenantScope } from "../disp
2
2
  import { getTableVersion } from "../change-feed.js";
3
3
  import { buildDefaultListOrderBy, resolveListLimit, resolveListOffset } from "../query-bounds.js";
4
4
  import { buildCustomActionInvocationArgs, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
5
+ import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
5
6
  import { ObjectRegistry } from "../registry.js";
6
7
  import "../dispatch/index.js";
7
8
  import { canonicalReadRepresentation, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized, warnIfTenantScopedPublicRead } from "./conditional-get.js";
8
9
  import { handleChangesRoute } from "./changes-route.js";
9
10
  import { handleEventsRoute } from "./events-route.js";
11
+ import { handlePlaybookPreflightRoute, isApiActionEnabledForObject, isRestRoutePublic } from "./preflight-route.js";
10
12
  import { SYNC_APPLY_ROUTE_SEGMENTS, processSyncApplyBatch } from "../sync/apply.js";
11
13
  import { computeWebManifestHash } from "../vite-plugin/web-collections.js";
12
14
  import { normalizeTypedHttpError } from "./typed-http-error.js";
@@ -245,6 +247,15 @@ var APIGenerator = class {
245
247
  });
246
248
  return this.addCorsHeaders(response, req);
247
249
  }
250
+ if (url.pathname === `${this.config.basePath}/_preflight`) {
251
+ const response = await handlePlaybookPreflightRoute(req, {
252
+ provider: this.config.playbookPreflight,
253
+ ...this.context.permissions ? { permissions: this.context.permissions } : {},
254
+ appAuthConfigured: Boolean(this.config.authMiddleware),
255
+ db: this.resolveContextDb()
256
+ });
257
+ return this.addCorsHeaders(response, req);
258
+ }
248
259
  if (url.pathname.startsWith(this.config.basePath || "")) {
249
260
  const response = await this.handleObjectRoute(req, url);
250
261
  return this.addCorsHeaders(response, req);
@@ -333,23 +344,10 @@ var APIGenerator = class {
333
344
  * Everything else requires an `authMiddleware` to be configured.
334
345
  */
335
346
  isRoutePublic(objectName, method) {
336
- if (!objectName) return false;
337
- const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
338
- if (!apiConfig || typeof apiConfig !== "object") return false;
339
- const publicAccess = apiConfig.public;
340
- if (publicAccess === true) return true;
341
- if (publicAccess === "read") return method.toUpperCase() === "GET";
342
- return false;
347
+ return isRestRoutePublic(objectName, method);
343
348
  }
344
349
  isApiActionEnabled(objectName, action) {
345
- if (!objectName) return true;
346
- const apiConfig = ObjectRegistry.getConfig(objectName).api;
347
- if (apiConfig === false) return false;
348
- if (apiConfig && typeof apiConfig === "object") {
349
- if (apiConfig.include && !apiConfig.include.includes(action)) return false;
350
- if (apiConfig.exclude?.includes(action)) return false;
351
- }
352
- return true;
350
+ return isApiActionEnabledForObject(objectName, action);
353
351
  }
354
352
  getCollectionObjectName(collection) {
355
353
  const itemClass = collection._itemClass || collection.constructor?._itemClass;
@@ -973,6 +971,7 @@ var APIGenerator = class {
973
971
  */
974
972
  findClassByCollectionSegment(segment) {
975
973
  for (const [key, info] of ObjectRegistry.getAllClasses()) {
974
+ if (isFrameworkBaseClass(info.name || key, info.packageName)) continue;
976
975
  if (info.collection === segment) return info;
977
976
  if (!info.collection && this.pluralize((info.name || key).toLowerCase()) === segment) return info;
978
977
  }