@happyvertical/smrt-core 0.45.3 → 0.47.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.
- package/AGENTS.md +39 -0
- package/agents/generators.md +0 -1
- package/dist/browser.d.ts +2 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/decorators/compatibility.d.ts +46 -0
- package/dist/decorators/compatibility.d.ts.map +1 -1
- package/dist/decorators/compatibility.js +48 -5
- package/dist/decorators/compatibility.js.map +1 -1
- package/dist/decorators/index.d.ts +119 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +52 -8
- package/dist/decorators/index.js.map +1 -1
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +20 -1
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/generators/custom-action.d.ts +450 -41
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +765 -56
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +1 -3
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -4
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +1 -1
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/preflight-route.d.ts +12 -10
- package/dist/generators/preflight-route.d.ts.map +1 -1
- package/dist/generators/preflight-route.js +46 -14
- package/dist/generators/preflight-route.js.map +1 -1
- package/dist/generators/rest.d.ts +44 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +84 -9
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/tenant-gate.d.ts +33 -9
- package/dist/generators/tenant-gate.d.ts.map +1 -1
- package/dist/generators/tenant-gate.js +2 -1
- package/dist/generators/tenant-gate.js.map +1 -1
- package/dist/generators.js +3 -4
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -6
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +87 -31
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +125 -53
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/store.js.map +1 -1
- package/dist/manifest.json +184 -53
- package/dist/migrations/framework-base-tables.d.ts +148 -0
- package/dist/migrations/framework-base-tables.d.ts.map +1 -0
- package/dist/migrations/framework-base-tables.js +419 -0
- package/dist/migrations/framework-base-tables.js.map +1 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +2 -1
- package/dist/migrations.js +2 -1
- package/dist/postgres-permissions.d.ts +45 -0
- package/dist/postgres-permissions.d.ts.map +1 -0
- package/dist/postgres-permissions.js +424 -0
- package/dist/postgres-permissions.js.map +1 -0
- package/dist/registry/framework-base-classes.d.ts.map +1 -1
- package/dist/registry/framework-base-classes.js +4 -1
- package/dist/registry/framework-base-classes.js.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/shared-state.d.ts +19 -0
- package/dist/registry/shared-state.d.ts.map +1 -1
- package/dist/registry/shared-state.js +16 -1
- package/dist/registry/shared-state.js.map +1 -1
- package/dist/registry.d.ts +136 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +231 -1
- package/dist/registry.js.map +1 -1
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +18 -16
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/scanner/types.d.ts +59 -6
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/scripts/create-wrappers.js +0 -5
- package/dist/smrt-knowledge.json +35 -35
- package/dist/testing/database.d.ts +6 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +9 -1
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/api-client-entries.d.ts.map +1 -1
- package/dist/vite-plugin/api-client-entries.js +23 -19
- package/dist/vite-plugin/api-client-entries.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +3 -167
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +31 -4
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +137 -71
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/templates/default-ui.ts +22 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +1 -1
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +10 -10
- package/dist/generators/cli.d.ts +0 -155
- package/dist/generators/cli.d.ts.map +0 -1
- package/dist/generators/cli.js +0 -473
- package/dist/generators/cli.js.map +0 -1
|
@@ -98,12 +98,13 @@ export declare function resolveRegisteredObjectName(model: string): string | und
|
|
|
98
98
|
* The HTTP method a REST action is served by.
|
|
99
99
|
*
|
|
100
100
|
* CRUD actions map to their fixed verbs. A custom action is served under the
|
|
101
|
-
* method its own
|
|
102
|
-
* to `POST` exactly as
|
|
103
|
-
* `
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
101
|
+
* method its own declaration supplies — `@method({ httpMethod })` first, then
|
|
102
|
+
* `api.routes[action].method`, defaulting to `POST` exactly as
|
|
103
|
+
* `dispatchCustomCollectionAction` does — because guessing `POST` for a
|
|
104
|
+
* declared `GET` action would make preflight report a false `deny` on a
|
|
105
|
+
* `public: 'read'` model, hiding a playbook the caller can actually run, which
|
|
106
|
+
* is the tool-listing case this exists to serve. Without an `objectName` to
|
|
107
|
+
* read the declaration from, the fail-closed `POST` remains.
|
|
107
108
|
*/
|
|
108
109
|
export declare function restMethodForApiAction(action: string, objectName?: string): string;
|
|
109
110
|
/**
|
|
@@ -124,13 +125,14 @@ export declare function isApiActionEnabledForObject(objectName: string | undefin
|
|
|
124
125
|
* Whether `action` names a route the generated REST surface can actually
|
|
125
126
|
* dispatch on `objectName`.
|
|
126
127
|
*
|
|
127
|
-
* CRUD actions always have a route. A custom action exists only when
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* CRUD actions always have a route. A custom action exists only when it is
|
|
129
|
+
* DECLARED — historically an `api.routes` entry, and since #2686 also a
|
|
130
|
+
* `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`
|
|
131
|
+
* iterates exactly that union, so an action outside it can never execute no
|
|
130
132
|
* matter what `include`/`exclude` say. Exposure alone would report a typo'd or
|
|
131
133
|
* removed custom action as `allow`, and an agent would then start the earlier
|
|
132
134
|
* steps of a non-atomic playbook before dying on it — the failure preflight
|
|
133
|
-
* exists to prevent.
|
|
135
|
+
* exists to prevent. This prediction and that dispatch must stay one rule.
|
|
134
136
|
*/
|
|
135
137
|
export declare function isRestActionRoutable(objectName: string | undefined, action: string): boolean;
|
|
136
138
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"preflight-route.d.ts","sourceRoot":"","sources":["../../src/generators/preflight-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;
|
|
1
|
+
{"version":3,"file":"preflight-route.d.ts","sourceRoot":"","sources":["../../src/generators/preflight-route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAWH,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;;;;;;;;;;;GAWG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,GAClB,MAAM,CAgCR;AAgBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAsBT;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,MAAM,GACb,OAAO,CAuCT;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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { declaresRuntimeRestRoute, readMethodDecoratorConfig, resolveEffectiveActionMetadata } from "./custom-action.js";
|
|
1
2
|
import { ObjectRegistry } from "../registry.js";
|
|
2
3
|
import { PRIVATE_READ_CACHE_CONTROL } from "./conditional-get.js";
|
|
3
4
|
//#region src/generators/preflight-route.ts
|
|
@@ -91,12 +92,13 @@ function resolveRegisteredObjectName(model) {
|
|
|
91
92
|
* The HTTP method a REST action is served by.
|
|
92
93
|
*
|
|
93
94
|
* CRUD actions map to their fixed verbs. A custom action is served under the
|
|
94
|
-
* method its own
|
|
95
|
-
* to `POST` exactly as
|
|
96
|
-
* `
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* method its own declaration supplies — `@method({ httpMethod })` first, then
|
|
96
|
+
* `api.routes[action].method`, defaulting to `POST` exactly as
|
|
97
|
+
* `dispatchCustomCollectionAction` does — because guessing `POST` for a
|
|
98
|
+
* declared `GET` action would make preflight report a false `deny` on a
|
|
99
|
+
* `public: 'read'` model, hiding a playbook the caller can actually run, which
|
|
100
|
+
* is the tool-listing case this exists to serve. Without an `objectName` to
|
|
101
|
+
* read the declaration from, the fail-closed `POST` remains.
|
|
100
102
|
*/
|
|
101
103
|
function restMethodForApiAction(action, objectName) {
|
|
102
104
|
switch (action) {
|
|
@@ -109,8 +111,22 @@ function restMethodForApiAction(action, objectName) {
|
|
|
109
111
|
}
|
|
110
112
|
if (!objectName) return "POST";
|
|
111
113
|
const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
|
|
112
|
-
if (!apiConfig || typeof apiConfig !== "object"
|
|
113
|
-
return (
|
|
114
|
+
if (!apiConfig || typeof apiConfig !== "object") return "POST";
|
|
115
|
+
return (resolveEffectiveActionMetadata({
|
|
116
|
+
actionName: action,
|
|
117
|
+
...readRegisteredMethod(objectName, action) ? { method: readRegisteredMethod(objectName, action) } : {},
|
|
118
|
+
apiConfig
|
|
119
|
+
}).httpMethod ?? "POST").toUpperCase();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The method view backing `action`: the manifest entry when the registry has
|
|
123
|
+
* one, with its `@method()` config backfilled from the live decorator store
|
|
124
|
+
* when it does not — the unscanned-runtime posture, where reading only the
|
|
125
|
+
* manifest would silently ignore `@method({ expose: false })` (#2686). Mirrors
|
|
126
|
+
* `APIGenerator.runtimeMethod`, which decides the dispatch this predicts.
|
|
127
|
+
*/
|
|
128
|
+
function readRegisteredMethod(objectName, action) {
|
|
129
|
+
return ObjectRegistry.resolveRuntimeMethod(objectName, action).method;
|
|
114
130
|
}
|
|
115
131
|
/**
|
|
116
132
|
* Whether `@smrt({ api })` exposes `action` on `objectName`.
|
|
@@ -139,20 +155,36 @@ function isApiActionEnabledForObject(objectName, action) {
|
|
|
139
155
|
* Whether `action` names a route the generated REST surface can actually
|
|
140
156
|
* dispatch on `objectName`.
|
|
141
157
|
*
|
|
142
|
-
* CRUD actions always have a route. A custom action exists only when
|
|
143
|
-
*
|
|
144
|
-
*
|
|
158
|
+
* CRUD actions always have a route. A custom action exists only when it is
|
|
159
|
+
* DECLARED — historically an `api.routes` entry, and since #2686 also a
|
|
160
|
+
* `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`
|
|
161
|
+
* iterates exactly that union, so an action outside it can never execute no
|
|
145
162
|
* matter what `include`/`exclude` say. Exposure alone would report a typo'd or
|
|
146
163
|
* removed custom action as `allow`, and an agent would then start the earlier
|
|
147
164
|
* steps of a non-atomic playbook before dying on it — the failure preflight
|
|
148
|
-
* exists to prevent.
|
|
165
|
+
* exists to prevent. This prediction and that dispatch must stay one rule.
|
|
149
166
|
*/
|
|
150
167
|
function isRestActionRoutable(objectName, action) {
|
|
151
168
|
if (!objectName) return false;
|
|
152
169
|
if (action === "list" || action === "get" || action === "create" || action === "update" || action === "delete") return true;
|
|
153
170
|
const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
|
|
154
|
-
if (!apiConfig || typeof apiConfig !== "object"
|
|
155
|
-
|
|
171
|
+
if (!apiConfig || typeof apiConfig !== "object") return false;
|
|
172
|
+
const registered = readRegisteredMethod(objectName, action);
|
|
173
|
+
if (readMethodDecoratorConfig(registered)?.expose === false) return false;
|
|
174
|
+
if (apiConfig.routes && Object.hasOwn(apiConfig.routes, action)) return isHostableByRuntimeRest(objectName, action);
|
|
175
|
+
return declaresRuntimeRestRoute(registered) && isHostableByRuntimeRest(objectName, action);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether this transport can HOST the action at all.
|
|
179
|
+
*
|
|
180
|
+
* It serves only collection-scoped custom actions, so an item-scoped
|
|
181
|
+
* declaration — the `@method()` decorator's most common shape, on a model
|
|
182
|
+
* instance method — answers 404 in dispatch. Predicting `allow` for it is the
|
|
183
|
+
* false-`allow` this module exists to prevent, so routability is the
|
|
184
|
+
* conjunction of "declared" and "hostable" (#2686).
|
|
185
|
+
*/
|
|
186
|
+
function isHostableByRuntimeRest(objectName, action) {
|
|
187
|
+
return ObjectRegistry.isCollectionHostedMethod(objectName, action);
|
|
156
188
|
}
|
|
157
189
|
/**
|
|
158
190
|
* Fail-closed authorization posture (#1540): true only when the object opts out
|
|
@@ -1 +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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"}
|
|
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 type { MethodDefinition } from '../scanner/types.js';\nimport { PRIVATE_READ_CACHE_CONTROL } from './conditional-get.js';\nimport {\n declaresRuntimeRestRoute,\n readMethodDecoratorConfig,\n resolveEffectiveActionMetadata,\n} from './custom-action.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 declaration supplies — `@method({ httpMethod })` first, then\n * `api.routes[action].method`, defaulting to `POST` exactly as\n * `dispatchCustomCollectionAction` does — because guessing `POST` for a\n * declared `GET` action would make preflight report a false `deny` on a\n * `public: 'read'` model, hiding a playbook the caller can actually run, which\n * is the tool-listing case this exists to serve. Without an `objectName` to\n * 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') {\n return 'POST';\n }\n\n const effective = resolveEffectiveActionMetadata({\n actionName: action,\n ...(readRegisteredMethod(objectName, action)\n ? { method: readRegisteredMethod(objectName, action) }\n : {}),\n apiConfig,\n });\n return (effective.httpMethod ?? 'POST').toUpperCase();\n}\n\n/**\n * The method view backing `action`: the manifest entry when the registry has\n * one, with its `@method()` config backfilled from the live decorator store\n * when it does not — the unscanned-runtime posture, where reading only the\n * manifest would silently ignore `@method({ expose: false })` (#2686). Mirrors\n * `APIGenerator.runtimeMethod`, which decides the dispatch this predicts.\n */\nfunction readRegisteredMethod(\n objectName: string,\n action: string,\n): MethodDefinition | undefined {\n return ObjectRegistry.resolveRuntimeMethod(objectName, action).method;\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 it is\n * DECLARED — historically an `api.routes` entry, and since #2686 also a\n * `@method()` supplying route-shaping metadata. `dispatchCustomCollectionAction`\n * iterates exactly that union, so an action outside 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. This prediction and that dispatch must stay one rule.\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') {\n return false;\n }\n\n const registered = readRegisteredMethod(objectName, action);\n\n // `@method({ expose: false })` outranks a legacy `api.routes` entry for the\n // same method, and dispatch honors that. Checking it BEFORE the routes map is\n // what keeps the two one rule: otherwise a withheld action that still carried\n // a route declaration predicted `allow` for an operation the transport\n // refuses (#2686).\n if (readMethodDecoratorConfig(registered)?.expose === false) return false;\n\n if (\n apiConfig.routes &&\n Object.hasOwn(apiConfig.routes as Record<string, unknown>, action)\n ) {\n return isHostableByRuntimeRest(objectName, action);\n }\n\n // Shared with `APIGenerator.declaredCollectionActions`, which decides the\n // dispatch this predicate exists to predict.\n return (\n declaresRuntimeRestRoute(registered) &&\n isHostableByRuntimeRest(objectName, action)\n );\n}\n\n/**\n * Whether this transport can HOST the action at all.\n *\n * It serves only collection-scoped custom actions, so an item-scoped\n * declaration — the `@method()` decorator's most common shape, on a model\n * instance method — answers 404 in dispatch. Predicting `allow` for it is the\n * false-`allow` this module exists to prevent, so routability is the\n * conjunction of \"declared\" and \"hostable\" (#2686).\n */\nfunction isHostableByRuntimeRest(objectName: string, action: string): boolean {\n return ObjectRegistry.isCollectionHostedMethod(objectName, 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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"}
|
|
@@ -179,6 +179,50 @@ export declare class APIGenerator {
|
|
|
179
179
|
* a returned `{ ok: false, ... }` failure becomes its requested non-2xx
|
|
180
180
|
* status with a redacted payload, and success wraps as `{ action, result }`.
|
|
181
181
|
*/
|
|
182
|
+
/**
|
|
183
|
+
* Every action this transport treats as DECLARED for a collection route,
|
|
184
|
+
* paired with its manifest method definition when one is known.
|
|
185
|
+
*
|
|
186
|
+
* The union of two declaration sources, so migrating a route entry onto its
|
|
187
|
+
* method does not silently drop the route here (#2686):
|
|
188
|
+
*
|
|
189
|
+
* - a `api.routes[action]` entry, this transport's only source before;
|
|
190
|
+
* - a method whose `@method()` supplies route-shaping metadata
|
|
191
|
+
* (`httpMethod`, `path`, or `scope`).
|
|
192
|
+
*
|
|
193
|
+
* Deliberately still DECLARATION-gated rather than adopting the generated
|
|
194
|
+
* transport's default-route heuristic: this router's URL shape supports only
|
|
195
|
+
* single-segment collection paths, and widening it to every wire-able public
|
|
196
|
+
* method would publish endpoints that have never existed here. That is a
|
|
197
|
+
* separate contract change, not a compatibility fix.
|
|
198
|
+
*
|
|
199
|
+
* `declaresRuntimeRestRoute` is shared with `isRestActionRoutable`, the
|
|
200
|
+
* browser-plane preflight prediction of this same dispatch, so the route and
|
|
201
|
+
* the prediction of the route cannot drift.
|
|
202
|
+
*/
|
|
203
|
+
private declaredCollectionActions;
|
|
204
|
+
/**
|
|
205
|
+
* The method view a RUNTIME transport should read for one action: the
|
|
206
|
+
* manifest entry when the project was scanned, with its `@method()` config
|
|
207
|
+
* backfilled from the live decorator store when it is not.
|
|
208
|
+
*/
|
|
209
|
+
private runtimeMethod;
|
|
210
|
+
/**
|
|
211
|
+
* Refuse a request that matched a DECLARED action's own path and verb but
|
|
212
|
+
* whose action is withheld — or return `null` to let it fall through.
|
|
213
|
+
*
|
|
214
|
+
* Only `POST` is refused, and the asymmetry is the whole point.
|
|
215
|
+
* `getCrudAction` maps `POST` to `create` and DISCARDS the path segment, so
|
|
216
|
+
* falling through writes a row and answers 201 for a request aimed at an
|
|
217
|
+
* operation the author withheld: silent, mutating, and the opposite of what
|
|
218
|
+
* the generated SvelteKit surface does for the same URL, which 404s because
|
|
219
|
+
* it wrote no route file. Every other verb carries the segment into a by-id
|
|
220
|
+
* operation that 404s on its own when the segment is not an id — and that
|
|
221
|
+
* fall-through is load-bearing, because an object's id may legitimately equal
|
|
222
|
+
* a declared action's path, and `GET /<collection>/<id>` must keep resolving
|
|
223
|
+
* to that object (#2686).
|
|
224
|
+
*/
|
|
225
|
+
private refuseWithheldAction;
|
|
182
226
|
private dispatchCustomCollectionAction;
|
|
183
227
|
/**
|
|
184
228
|
* Public-safe projection for custom-action results, mirroring the generated
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAKpD,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,WAAW,CAAC;AAQ/D,OAAO,KAAK,EAEV,eAAe,EAGhB,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../../src/generators/rest.ts"],"names":[],"mappings":"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"}
|
package/dist/generators/rest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isTenantScopedClassResolved, resolveDispatchTenantScope } from "../dispatch/tenant-resolver.js";
|
|
2
2
|
import { getTableVersion } from "../change-feed.js";
|
|
3
3
|
import { buildDefaultListOrderBy, resolveListLimit, resolveListOffset } from "../query-bounds.js";
|
|
4
|
-
import { buildCustomActionInvocationArgs, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
|
|
4
|
+
import { buildCustomActionInvocationArgs, declaresRuntimeRestRouteShape, normalizeCustomActionFailure, readMethodDecoratorConfig, resolveCustomActionMetadata, resolveEffectiveActionMetadata } from "./custom-action.js";
|
|
5
5
|
import { isFrameworkBaseClass } from "../registry/framework-base-classes.js";
|
|
6
6
|
import { ObjectRegistry } from "../registry.js";
|
|
7
7
|
import "../dispatch/index.js";
|
|
@@ -385,28 +385,103 @@ var APIGenerator = class {
|
|
|
385
385
|
* a returned `{ ok: false, ... }` failure becomes its requested non-2xx
|
|
386
386
|
* status with a redacted payload, and success wraps as `{ action, result }`.
|
|
387
387
|
*/
|
|
388
|
+
/**
|
|
389
|
+
* Every action this transport treats as DECLARED for a collection route,
|
|
390
|
+
* paired with its manifest method definition when one is known.
|
|
391
|
+
*
|
|
392
|
+
* The union of two declaration sources, so migrating a route entry onto its
|
|
393
|
+
* method does not silently drop the route here (#2686):
|
|
394
|
+
*
|
|
395
|
+
* - a `api.routes[action]` entry, this transport's only source before;
|
|
396
|
+
* - a method whose `@method()` supplies route-shaping metadata
|
|
397
|
+
* (`httpMethod`, `path`, or `scope`).
|
|
398
|
+
*
|
|
399
|
+
* Deliberately still DECLARATION-gated rather than adopting the generated
|
|
400
|
+
* transport's default-route heuristic: this router's URL shape supports only
|
|
401
|
+
* single-segment collection paths, and widening it to every wire-able public
|
|
402
|
+
* method would publish endpoints that have never existed here. That is a
|
|
403
|
+
* separate contract change, not a compatibility fix.
|
|
404
|
+
*
|
|
405
|
+
* `declaresRuntimeRestRoute` is shared with `isRestActionRoutable`, the
|
|
406
|
+
* browser-plane preflight prediction of this same dispatch, so the route and
|
|
407
|
+
* the prediction of the route cannot drift.
|
|
408
|
+
*/
|
|
409
|
+
declaredCollectionActions(objectName, apiConfig) {
|
|
410
|
+
const names = new Set(Object.keys(apiConfig.routes ?? {}));
|
|
411
|
+
for (const name of ObjectRegistry.listDecoratedMethodNames(objectName)) if (declaresRuntimeRestRouteShape(this.runtimeMethod(objectName, name))) names.add(name);
|
|
412
|
+
return [...names].map((name) => [name, this.runtimeMethod(objectName, name)]);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The method view a RUNTIME transport should read for one action: the
|
|
416
|
+
* manifest entry when the project was scanned, with its `@method()` config
|
|
417
|
+
* backfilled from the live decorator store when it is not.
|
|
418
|
+
*/
|
|
419
|
+
runtimeMethod(objectName, methodName) {
|
|
420
|
+
return ObjectRegistry.resolveRuntimeMethod(objectName, methodName).method;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Refuse a request that matched a DECLARED action's own path and verb but
|
|
424
|
+
* whose action is withheld — or return `null` to let it fall through.
|
|
425
|
+
*
|
|
426
|
+
* Only `POST` is refused, and the asymmetry is the whole point.
|
|
427
|
+
* `getCrudAction` maps `POST` to `create` and DISCARDS the path segment, so
|
|
428
|
+
* falling through writes a row and answers 201 for a request aimed at an
|
|
429
|
+
* operation the author withheld: silent, mutating, and the opposite of what
|
|
430
|
+
* the generated SvelteKit surface does for the same URL, which 404s because
|
|
431
|
+
* it wrote no route file. Every other verb carries the segment into a by-id
|
|
432
|
+
* operation that 404s on its own when the segment is not an id — and that
|
|
433
|
+
* fall-through is load-bearing, because an object's id may legitimately equal
|
|
434
|
+
* a declared action's path, and `GET /<collection>/<id>` must keep resolving
|
|
435
|
+
* to that object (#2686).
|
|
436
|
+
*/
|
|
437
|
+
refuseWithheldAction(req, actionName, withheldBy) {
|
|
438
|
+
if (req.method.toUpperCase() !== "POST") return null;
|
|
439
|
+
return this.createErrorResponse(404, `Custom action '${actionName}' is not exposed (${withheldBy})`);
|
|
440
|
+
}
|
|
388
441
|
async dispatchCustomCollectionAction(req, collection, pathSegment, url, objectName) {
|
|
389
442
|
const apiConfig = ObjectRegistry.getConfig(objectName)?.api;
|
|
390
|
-
if (!apiConfig || typeof apiConfig !== "object"
|
|
391
|
-
|
|
392
|
-
|
|
443
|
+
if (!apiConfig || typeof apiConfig !== "object") return null;
|
|
444
|
+
const declaredActions = this.declaredCollectionActions(objectName, apiConfig);
|
|
445
|
+
if (declaredActions.length === 0) return null;
|
|
446
|
+
for (const [actionName, declaredMethod] of declaredActions) {
|
|
447
|
+
const effective = resolveEffectiveActionMetadata({
|
|
448
|
+
actionName,
|
|
449
|
+
...declaredMethod ? { method: declaredMethod } : {},
|
|
450
|
+
apiConfig
|
|
451
|
+
});
|
|
452
|
+
const routePath = effective.path || actionName;
|
|
393
453
|
if (routePath.includes("/") || routePath !== pathSegment) continue;
|
|
394
|
-
const routeMethod = (
|
|
454
|
+
const routeMethod = (effective.httpMethod || "POST").toUpperCase();
|
|
395
455
|
if (routeMethod !== req.method.toUpperCase()) continue;
|
|
396
|
-
if (apiConfig.include && !apiConfig.include.includes(actionName))
|
|
397
|
-
|
|
456
|
+
if (apiConfig.include && !apiConfig.include.includes(actionName)) {
|
|
457
|
+
const refusal = this.refuseWithheldAction(req, actionName, "api.include");
|
|
458
|
+
if (refusal) return refusal;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (apiConfig.exclude?.includes(actionName)) {
|
|
462
|
+
const refusal = this.refuseWithheldAction(req, actionName, "api.exclude");
|
|
463
|
+
if (refusal) return refusal;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (readMethodDecoratorConfig(declaredMethod)?.expose === false) {
|
|
467
|
+
const refusal = this.refuseWithheldAction(req, actionName, "@method({ expose: false })");
|
|
468
|
+
if (refusal) return refusal;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
398
471
|
const collectionMethod = collection[actionName];
|
|
399
472
|
const itemConstructor = ObjectRegistry.getClass(objectName)?.constructor;
|
|
400
473
|
const staticMethod = itemConstructor?.[actionName];
|
|
401
474
|
const isCollectionHosted = typeof collectionMethod === "function";
|
|
402
475
|
if (!isCollectionHosted && !(!isCollectionHosted && typeof staticMethod === "function")) {
|
|
403
|
-
if (
|
|
476
|
+
if (effective.scope === "collection") return this.createErrorResponse(501, `Custom action '${actionName}' is declared but not implemented`);
|
|
477
|
+
const refusal = this.refuseWithheldAction(req, actionName, "an item-scoped declaration, which this transport does not serve");
|
|
478
|
+
if (refusal) return refusal;
|
|
404
479
|
continue;
|
|
405
480
|
}
|
|
406
481
|
const metadata = resolveCustomActionMetadata({
|
|
407
482
|
actionName,
|
|
408
483
|
apiConfig,
|
|
409
|
-
method:
|
|
484
|
+
...declaredMethod ? { method: declaredMethod } : {},
|
|
410
485
|
defaultScope: "collection"
|
|
411
486
|
});
|
|
412
487
|
if (metadata.scope !== "collection") continue;
|