@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.
- package/AGENTS.md +8 -8
- package/agents/change-feed.md +3 -2
- package/agents/generators.md +34 -0
- package/agents/revision-guard.md +66 -0
- package/dist/browser.js +2 -1
- package/dist/cascade.d.ts +5 -9
- package/dist/cascade.d.ts.map +1 -1
- package/dist/cascade.js +65 -30
- package/dist/cascade.js.map +1 -1
- package/dist/change-feed.d.ts +60 -1
- package/dist/change-feed.d.ts.map +1 -1
- package/dist/change-feed.js +464 -27
- package/dist/change-feed.js.map +1 -1
- package/dist/change-signals.d.ts.map +1 -1
- package/dist/change-signals.js +13 -11
- package/dist/change-signals.js.map +1 -1
- package/dist/embedded-write-queue.d.ts +8 -0
- package/dist/embedded-write-queue.d.ts.map +1 -1
- package/dist/embedded-write-queue.js +12 -2
- package/dist/embedded-write-queue.js.map +1 -1
- package/dist/generators/cli.d.ts.map +1 -1
- package/dist/generators/cli.js +10 -18
- package/dist/generators/cli.js.map +1 -1
- package/dist/generators/custom-action.d.ts +216 -0
- package/dist/generators/custom-action.d.ts.map +1 -1
- package/dist/generators/custom-action.js +256 -1
- package/dist/generators/custom-action.js.map +1 -1
- package/dist/generators/index.d.ts +2 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -2
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +14 -39
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/preflight-route.d.ts +151 -0
- package/dist/generators/preflight-route.d.ts.map +1 -0
- package/dist/generators/preflight-route.js +194 -0
- package/dist/generators/preflight-route.js.map +1 -0
- package/dist/generators/rest.d.ts +12 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +14 -15
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +2 -8
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/generators.js +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/knowledge.d.ts.map +1 -1
- package/dist/knowledge.js +283 -35
- package/dist/knowledge.js.map +1 -1
- package/dist/manifest/static-manifest.d.ts.map +1 -1
- package/dist/manifest/static-manifest.js +6 -2
- 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 +8 -2
- package/dist/object.d.ts +31 -1
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +57 -9
- package/dist/object.js.map +1 -1
- package/dist/registry/framework-base-classes.d.ts +10 -0
- package/dist/registry/framework-base-classes.d.ts.map +1 -0
- package/dist/registry/framework-base-classes.js +92 -0
- package/dist/registry/framework-base-classes.js.map +1 -0
- package/dist/registry/schema-builder.d.ts.map +1 -1
- package/dist/registry/schema-builder.js +2 -0
- package/dist/registry/schema-builder.js.map +1 -1
- package/dist/registry/types.d.ts +27 -6
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.d.ts +1 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +2 -1
- package/dist/registry.js.map +1 -1
- package/dist/revision-guard.d.ts +84 -0
- package/dist/revision-guard.d.ts.map +1 -0
- package/dist/revision-guard.js +120 -0
- package/dist/revision-guard.js.map +1 -0
- package/dist/scanner/manifest-generator.d.ts +46 -16
- package/dist/scanner/manifest-generator.d.ts.map +1 -1
- package/dist/scanner/manifest-generator.js +198 -45
- package/dist/scanner/manifest-generator.js.map +1 -1
- package/dist/smrt-knowledge.json +18 -9
- package/dist/system/bootstrap.d.ts.map +1 -1
- package/dist/system/bootstrap.js +2 -1
- package/dist/system/bootstrap.js.map +1 -1
- package/dist/system/schema.d.ts +75 -2
- package/dist/system/schema.d.ts.map +1 -1
- package/dist/system/schema.js +326 -4
- package/dist/system/schema.js.map +1 -1
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +48 -23
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts +33 -7
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +88 -17
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
- package/dist/vite-plugin/sync-apply-route.js +2 -0
- package/dist/vite-plugin/sync-apply-route.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +28 -5
- package/dist/vite-plugin/web-collections.js.map +1 -1
- 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":"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;
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|