@classytic/arc 2.18.5 → 2.20.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/README.md +1 -1
- package/bin/arc.js +37 -0
- package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
- package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
- package/dist/{actionPermissions-h7DYDNMg.mjs → actionPermissions-C9RMiIpc.mjs} +2 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +17 -6
- package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +4 -13
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +4 -4
- package/dist/{core-ZnQBa5rh.mjs → core-CWhyGkiM.mjs} +136 -59
- package/dist/{createActionRouter-CagItpxI.mjs → createActionRouter-HlQso0jO.mjs} +8 -7
- package/dist/{createAggregationRouter-DVKroXhC.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
- package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.d.mts +222 -0
- package/dist/encryption/index.mjs +270 -0
- package/dist/events/index.mjs +9 -6
- package/dist/factory/index.d.mts +3 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fastify-augmentation-Cxb0ROIw.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
- package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
- package/dist/{index-Dh0_RSTd.d.mts → index-DUa8WHz3.d.mts} +7 -7
- package/dist/{index-CHNQIhcb.d.mts → index-_--GyzcO.d.mts} +111 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +6 -6
- package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +17 -5
- package/dist/integrations/streamline.mjs +9 -5
- package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
- package/dist/{mcpPlugin-D1wuRD2c.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/module-JYH9V0I8.d.mts +384 -0
- package/dist/{openapi-xVJibapr.mjs → openapi-Dq6RbEGK.mjs} +3 -3
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +10 -5
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +16 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +3 -2
- package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
- package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
- package/dist/{registerRoute-CPmdkZGu.mjs → registerRoute-DGSREwPw.mjs} +20 -2
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
- package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
- package/dist/scim/index.mjs +2 -2
- package/dist/sync/changelog.d.mts +2 -0
- package/dist/sync/changelog.mjs +2 -0
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +2 -2
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-CmkknLzI.d.mts → types-BHMKlMBU.d.mts} +53 -195
- package/dist/{types-D60PFwfG.d.mts → types-C9b2HR5W.d.mts} +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
- package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
- package/package.json +22 -8
- package/skills/arc/SKILL.md +10 -0
- package/skills/arc/references/mcp.md +2 -0
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-
|
|
1
|
+
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-DNre0wRg.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
3
|
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
4
|
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-
|
|
7
|
-
import {
|
|
5
|
+
import { A as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-ChjYQpjn.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-D-MHZ27_.mjs";
|
|
7
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
|
|
8
|
+
import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-CgbOLJZG.mjs";
|
|
8
9
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
10
|
+
import { t as applyPresets } from "./presets-DWsYQdbV.mjs";
|
|
9
11
|
import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.mjs";
|
|
10
12
|
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
11
|
-
import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-h7DYDNMg.mjs";
|
|
12
|
-
import { c as resolveRoutePreHandlers, d as buildArcDecorator, i as selectPluginMw, l as buildAuthMiddleware, n as buildRateLimitConfig, o as buildCrudPermissionMw, r as resolveRouterPluginMw, s as buildPreHandlerChain, t as tryRegisterRoute } from "./registerRoute-CPmdkZGu.mjs";
|
|
13
|
-
import { t as applyPresets } from "./presets-BtyFwTO4.mjs";
|
|
14
13
|
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
15
|
-
import { t as
|
|
14
|
+
import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
|
|
15
|
+
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
16
|
+
//#region src/core/middlewares/fieldWrite.ts
|
|
17
|
+
/**
|
|
18
|
+
* Build a preHandler that enforces field-write permissions on the request body.
|
|
19
|
+
*
|
|
20
|
+
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
21
|
+
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
22
|
+
* never went through that path — so a host that declared
|
|
23
|
+
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
24
|
+
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
25
|
+
*
|
|
26
|
+
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
27
|
+
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
28
|
+
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
29
|
+
* generate surprising 403s by enforcing rules tied to that shape.
|
|
30
|
+
*
|
|
31
|
+
* Returns `null` when the resource has no field permissions configured — the
|
|
32
|
+
* caller should then skip wiring it into the preHandler chain.
|
|
33
|
+
*/
|
|
34
|
+
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
35
|
+
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
36
|
+
return async (request, _reply) => {
|
|
37
|
+
const body = request.body;
|
|
38
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
39
|
+
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
40
|
+
if (isElevated(scope)) return;
|
|
41
|
+
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
42
|
+
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
43
|
+
request.body = filtered;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* True for HTTP methods that carry a request body and should run the
|
|
48
|
+
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
49
|
+
*/
|
|
50
|
+
function methodCarriesBody(method) {
|
|
51
|
+
const m = method.toUpperCase();
|
|
52
|
+
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
16
55
|
//#region src/core/aggregation/defineAggregation.ts
|
|
17
56
|
/**
|
|
18
57
|
* Declare a single resource aggregation. Exported configs flow into
|
|
@@ -59,52 +98,13 @@ function defineAggregation(config) {
|
|
|
59
98
|
return config;
|
|
60
99
|
}
|
|
61
100
|
//#endregion
|
|
62
|
-
//#region src/core/middlewares/fieldWrite.ts
|
|
63
|
-
/**
|
|
64
|
-
* Build a preHandler that enforces field-write permissions on the request body.
|
|
65
|
-
*
|
|
66
|
-
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
67
|
-
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
68
|
-
* never went through that path — so a host that declared
|
|
69
|
-
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
70
|
-
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
71
|
-
*
|
|
72
|
-
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
73
|
-
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
74
|
-
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
75
|
-
* generate surprising 403s by enforcing rules tied to that shape.
|
|
76
|
-
*
|
|
77
|
-
* Returns `null` when the resource has no field permissions configured — the
|
|
78
|
-
* caller should then skip wiring it into the preHandler chain.
|
|
79
|
-
*/
|
|
80
|
-
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
81
|
-
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
82
|
-
return async (request, _reply) => {
|
|
83
|
-
const body = request.body;
|
|
84
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
85
|
-
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
86
|
-
if (isElevated(scope)) return;
|
|
87
|
-
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
88
|
-
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
89
|
-
request.body = filtered;
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* True for HTTP methods that carry a request body and should run the
|
|
94
|
-
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
95
|
-
*/
|
|
96
|
-
function methodCarriesBody(method) {
|
|
97
|
-
const m = method.toUpperCase();
|
|
98
|
-
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
101
101
|
//#region src/core/createCrudRouter.ts
|
|
102
102
|
/**
|
|
103
103
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
104
104
|
* `wrapHandler` is derived inline from `!route.raw`.
|
|
105
105
|
*/
|
|
106
106
|
function createCustomRoutes(fastify, routes, controller, options) {
|
|
107
|
-
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied } = options;
|
|
107
|
+
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied, extensions } = options;
|
|
108
108
|
for (const route of routes) {
|
|
109
109
|
const routeWithRefs = route;
|
|
110
110
|
const hasHandler = route.handler !== void 0;
|
|
@@ -177,7 +177,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
177
177
|
}
|
|
178
178
|
return result;
|
|
179
179
|
} : handler,
|
|
180
|
-
...
|
|
180
|
+
...buildRouteConfig(route.rateLimit !== void 0 ? buildRateLimitConfig(route.rateLimit) : rateLimitConfig, extensions)
|
|
181
181
|
}, {
|
|
182
182
|
resourceName,
|
|
183
183
|
op: opName
|
|
@@ -194,7 +194,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
194
194
|
* @param options - Router configuration
|
|
195
195
|
*/
|
|
196
196
|
function createCrudRouter(fastify, controller, options = {}) {
|
|
197
|
-
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
|
|
197
|
+
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField, extensions } = options;
|
|
198
198
|
const rateLimitConfig = buildRateLimitConfig(rateLimit);
|
|
199
199
|
const resourceHasQueryCache = fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0;
|
|
200
200
|
const pluginMw = resolveRouterPluginMw(fastify, Boolean(resourceHasQueryCache));
|
|
@@ -295,7 +295,7 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
295
295
|
}, defaultSchemas[spec.op], schemas[spec.op]),
|
|
296
296
|
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
297
297
|
handler: handlers[spec.op],
|
|
298
|
-
...rateLimitConfig
|
|
298
|
+
...buildRouteConfig(rateLimitConfig, extensions)
|
|
299
299
|
}, {
|
|
300
300
|
resourceName,
|
|
301
301
|
op: spec.op
|
|
@@ -313,7 +313,8 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
313
313
|
routeGuards,
|
|
314
314
|
tenantScopeMw,
|
|
315
315
|
fieldPermissions,
|
|
316
|
-
onFieldWriteDenied
|
|
316
|
+
onFieldWriteDenied,
|
|
317
|
+
extensions
|
|
317
318
|
});
|
|
318
319
|
}
|
|
319
320
|
function buildCrudHandlers(ctrl, pipeline, resourceName) {
|
|
@@ -948,6 +949,42 @@ function computeHasCrudRoutes(config) {
|
|
|
948
949
|
const disabled = new Set(config.disabledRoutes ?? []);
|
|
949
950
|
return !config.disableDefaultRoutes && CRUD_OPERATIONS.some((op) => !disabled.has(op));
|
|
950
951
|
}
|
|
952
|
+
const WRITE_OPERATIONS = /* @__PURE__ */ new Set([
|
|
953
|
+
"create",
|
|
954
|
+
"update",
|
|
955
|
+
"delete"
|
|
956
|
+
]);
|
|
957
|
+
/**
|
|
958
|
+
* Public-by-omission CRUD diagnostic (post-preset).
|
|
959
|
+
*
|
|
960
|
+
* An omitted CRUD permission mounts the route with NO permission
|
|
961
|
+
* middleware at all — unlike custom routes and actions, which fail boot
|
|
962
|
+
* without one. That asymmetry silently shipped unauthenticated writes
|
|
963
|
+
* (`permissions: readOnly()` pre-2.20 being the canonical victim).
|
|
964
|
+
*
|
|
965
|
+
* Runs against `resolvedConfig` — never the raw user config — so
|
|
966
|
+
* permissions injected by presets (ownedByUser, multiTenant policy
|
|
967
|
+
* wiring, ...) count as gates and don't false-positive.
|
|
968
|
+
*
|
|
969
|
+
* Severity ladder:
|
|
970
|
+
* - `warn` when any enabled WRITE op (create/update/delete) is ungated —
|
|
971
|
+
* this is real exposure the host must acknowledge.
|
|
972
|
+
* - `info` when only reads are ungated — public catalogs
|
|
973
|
+
* (`referenceData: true`) are a legitimate shape, but the host should
|
|
974
|
+
* still state intent with `permissions.fullPublic()` / `allowPublic()`.
|
|
975
|
+
*/
|
|
976
|
+
function collectUngatedCrudDiagnostics(config) {
|
|
977
|
+
if (config.disableDefaultRoutes) return [];
|
|
978
|
+
const disabled = new Set(config.disabledRoutes ?? []);
|
|
979
|
+
const permissions = config.permissions ?? {};
|
|
980
|
+
const ungated = CRUD_OPERATIONS.filter((op) => !disabled.has(op) && typeof permissions[op] !== "function");
|
|
981
|
+
if (ungated.length === 0) return [];
|
|
982
|
+
return [{
|
|
983
|
+
severity: ungated.some((op) => WRITE_OPERATIONS.has(op)) ? "warn" : "info",
|
|
984
|
+
code: "crud-public-by-omission",
|
|
985
|
+
message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\nAn omitted permission mounts the route WITHOUT auth. State intent explicitly:\n permissions: permissions.fullPublic() // intentionally public\n permissions: permissions.authenticated() // require auth on every op\n permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation`
|
|
986
|
+
}];
|
|
987
|
+
}
|
|
951
988
|
//#endregion
|
|
952
989
|
//#region src/registry/resolveTenantPurge.ts
|
|
953
990
|
const DEFAULT_PRIORITY = 100;
|
|
@@ -1101,7 +1138,7 @@ function deepMergeSchemas(base, override) {
|
|
|
1101
1138
|
if (!override) return base;
|
|
1102
1139
|
if (!base) return override;
|
|
1103
1140
|
const result = { ...base };
|
|
1104
|
-
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [
|
|
1141
|
+
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [.../* @__PURE__ */ new Set([...result[key], ...value])];
|
|
1105
1142
|
else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = deepMergeSchemas(result[key], value);
|
|
1106
1143
|
else result[key] = value;
|
|
1107
1144
|
return result;
|
|
@@ -1242,10 +1279,11 @@ function buildResourcePlugin(resource) {
|
|
|
1242
1279
|
pipe: resource.pipe,
|
|
1243
1280
|
fields: resource.fields,
|
|
1244
1281
|
onFieldWriteDenied: resource.onFieldWriteDenied,
|
|
1245
|
-
idField: resource.idField
|
|
1282
|
+
idField: resource.idField,
|
|
1283
|
+
extensions: resource.extensions
|
|
1246
1284
|
});
|
|
1247
1285
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1248
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1286
|
+
const { createActionRouter } = await import("./createActionRouter-HlQso0jO.mjs").then((n) => n.n);
|
|
1249
1287
|
createActionRouter(typedInstance, {
|
|
1250
1288
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1251
1289
|
resourceName: resource.name,
|
|
@@ -1255,11 +1293,12 @@ function buildResourcePlugin(resource) {
|
|
|
1255
1293
|
permissions: resource.permissions,
|
|
1256
1294
|
routeGuards: resource.routeGuards,
|
|
1257
1295
|
pipeline: resource.pipe,
|
|
1258
|
-
rateLimit: resource.rateLimit
|
|
1296
|
+
rateLimit: resource.rateLimit,
|
|
1297
|
+
extensions: resource.extensions
|
|
1259
1298
|
});
|
|
1260
1299
|
}
|
|
1261
1300
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1262
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1301
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-5LzPzgw1.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
|
|
1263
1302
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1264
1303
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1265
1304
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1332,6 +1371,7 @@ var ResourceDefinition = class {
|
|
|
1332
1371
|
*/
|
|
1333
1372
|
onFieldWriteDenied;
|
|
1334
1373
|
cache;
|
|
1374
|
+
extensions;
|
|
1335
1375
|
referenceData;
|
|
1336
1376
|
/**
|
|
1337
1377
|
* Per-resource MCP opt-out. `false` keeps the resource out of every
|
|
@@ -1429,6 +1469,7 @@ var ResourceDefinition = class {
|
|
|
1429
1469
|
this.fields = config.fields;
|
|
1430
1470
|
this.onFieldWriteDenied = config.onFieldWriteDenied;
|
|
1431
1471
|
this.cache = config.cache;
|
|
1472
|
+
this.extensions = config.extensions ? Object.freeze({ ...config.extensions }) : void 0;
|
|
1432
1473
|
this.mcp = config.mcp !== false;
|
|
1433
1474
|
this.referenceData = config.referenceData === true;
|
|
1434
1475
|
this.tenantField = config.tenantField;
|
|
@@ -1713,7 +1754,7 @@ function mergeUserOpenApiOverrides(openApiSchemas, userOverrides) {
|
|
|
1713
1754
|
* call) since the set is fixed and the cost of re-allocating is a
|
|
1714
1755
|
* pointless boot tax for hosts with hundreds of resources.
|
|
1715
1756
|
*/
|
|
1716
|
-
const CRUD_OP_NAMES = new Set([
|
|
1757
|
+
const CRUD_OP_NAMES = /* @__PURE__ */ new Set([
|
|
1717
1758
|
"create",
|
|
1718
1759
|
"update",
|
|
1719
1760
|
"delete",
|
|
@@ -1733,7 +1774,42 @@ function validateDefineResourceConfig(config) {
|
|
|
1733
1774
|
validatePermissionsShape(config);
|
|
1734
1775
|
validateCustomRoutePermissions(config);
|
|
1735
1776
|
validateActionsShape(config);
|
|
1736
|
-
return collectRedundantFieldRuleDiagnostics(config);
|
|
1777
|
+
return [...collectRedundantFieldRuleDiagnostics(config), ...collectReservedFilterNameDiagnostics(config)];
|
|
1778
|
+
}
|
|
1779
|
+
/**
|
|
1780
|
+
* Query-string param names arc's `QueryParser` reserves for
|
|
1781
|
+
* pagination/projection (`q.after ?? q.cursor` is the keyset cursor). A
|
|
1782
|
+
* document field with one of these names can never be filtered via the query
|
|
1783
|
+
* string — the parser consumes the name before it reaches the filter map — and
|
|
1784
|
+
* the same reservation flows into the MCP list-tool schema. Silent pre-2.20;
|
|
1785
|
+
* now surfaced.
|
|
1786
|
+
*/
|
|
1787
|
+
const RESERVED_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
1788
|
+
"page",
|
|
1789
|
+
"limit",
|
|
1790
|
+
"cursor",
|
|
1791
|
+
"after",
|
|
1792
|
+
"sort",
|
|
1793
|
+
"search",
|
|
1794
|
+
"select",
|
|
1795
|
+
"populate"
|
|
1796
|
+
]);
|
|
1797
|
+
/**
|
|
1798
|
+
* Warn when a declared filterable field's NAME collides with a reserved query
|
|
1799
|
+
* param. Verified empirically (tests/e2e/reserved-field-name-collision.test.ts):
|
|
1800
|
+
* a field named `cursor`/`page` is silently unfilterable. Boot diagnostic, not
|
|
1801
|
+
* a hard error — the resource still works, the field just isn't query-filterable.
|
|
1802
|
+
*/
|
|
1803
|
+
function collectReservedFilterNameDiagnostics(config) {
|
|
1804
|
+
const filterable = config.schemaOptions?.filterableFields;
|
|
1805
|
+
if (!filterable) return [];
|
|
1806
|
+
const collisions = filterable.filter((f) => RESERVED_QUERY_PARAMS.has(f));
|
|
1807
|
+
if (collisions.length === 0) return [];
|
|
1808
|
+
return [{
|
|
1809
|
+
severity: "warn",
|
|
1810
|
+
code: "filter-field-reserved-name",
|
|
1811
|
+
message: `[Arc] Resource '${config.name}': filterable field(s) [${collisions.join(", ")}] collide with reserved query params (page, limit, cursor, after, sort, search, select, populate). The query parser consumes these names for pagination/projection, so filtering by them via the query string — and via the MCP list tool — will NOT work. Rename the field(s), or drop them from \`filterableFields\` if the collision is intended.`
|
|
1812
|
+
}];
|
|
1737
1813
|
}
|
|
1738
1814
|
/** Permissions must be `PermissionCheck` functions, not arbitrary values. */
|
|
1739
1815
|
function validatePermissionsShape(config) {
|
|
@@ -1815,6 +1891,7 @@ function defineResource(config) {
|
|
|
1815
1891
|
const configWithId = resolveIdField(normalisedConfig, repository);
|
|
1816
1892
|
const resolvedConfig = applyPresetsAndAutoInject(configWithId);
|
|
1817
1893
|
const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
|
|
1894
|
+
if (!normalisedConfig.skipValidation) diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig));
|
|
1818
1895
|
const narrowedConfig = resolvedConfig;
|
|
1819
1896
|
const narrowedAdapter = configWithId.adapter;
|
|
1820
1897
|
const controller = resolveOrAutoCreateController(narrowedConfig, narrowedAdapter, repository, hasCrudRoutes);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
-
import "./constants-
|
|
2
|
+
import "./constants-DNre0wRg.mjs";
|
|
3
3
|
import { f as createError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
-
import { i as
|
|
5
|
-
import {
|
|
6
|
-
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-
|
|
4
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, d as buildAuthMiddlewareForPermissions, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, r as buildRouteConfig, t as tryRegisterRoute } from "./registerRoute-DGSREwPw.mjs";
|
|
5
|
+
import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-C9RMiIpc.mjs";
|
|
6
|
+
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
|
|
7
7
|
//#region src/core/createActionRouter.ts
|
|
8
8
|
var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
9
9
|
buildActionBodySchema: () => buildActionBodySchema,
|
|
@@ -18,7 +18,7 @@ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
|
18
18
|
* (keyed by `body.action` at request time).
|
|
19
19
|
*/
|
|
20
20
|
function createActionRouter(fastify, config) {
|
|
21
|
-
const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, idField = "_id", permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit, idLessActionNames = [] } = config;
|
|
21
|
+
const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, idField = "_id", permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit, idLessActionNames = [], extensions } = config;
|
|
22
22
|
const allActionNames = Object.keys(actions);
|
|
23
23
|
if (allActionNames.length === 0) {
|
|
24
24
|
fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
|
|
@@ -121,7 +121,7 @@ function createActionRouter(fastify, config) {
|
|
|
121
121
|
schema: routeSchema,
|
|
122
122
|
attachValidation: true,
|
|
123
123
|
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
124
|
-
...rateLimitConfig
|
|
124
|
+
...buildRouteConfig(rateLimitConfig, extensions),
|
|
125
125
|
handler: async (req, reply) => {
|
|
126
126
|
const { action, ...data } = req.body;
|
|
127
127
|
const id = hasId ? req.params.id : "";
|
|
@@ -135,7 +135,8 @@ function createActionRouter(fastify, config) {
|
|
|
135
135
|
const handler = wrappedHandlers.get(action);
|
|
136
136
|
if (!handler) throw createError(400, `Invalid action '${action}'. Valid actions: ${actionSubset.join(", ")}`, { validActions: actionSubset });
|
|
137
137
|
try {
|
|
138
|
-
|
|
138
|
+
sendControllerResponse(reply, await handler(id, data, req), req);
|
|
139
|
+
return reply;
|
|
139
140
|
} catch (error) {
|
|
140
141
|
if (onError) {
|
|
141
142
|
const { statusCode, error: errorMsg, code } = onError(error, action, id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as validateAggregations } from "./validate-
|
|
4
|
-
import { t as buildAggregationHandler } from "./buildHandler-
|
|
2
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-DGSREwPw.mjs";
|
|
3
|
+
import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
|
|
4
|
+
import { t as buildAggregationHandler } from "./buildHandler-D_2ULWxZ.mjs";
|
|
5
5
|
//#region src/core/aggregation/createAggregationRouter.ts
|
|
6
6
|
/**
|
|
7
7
|
* Register one Fastify route per aggregation. No-op when the map is
|
|
@@ -39,7 +39,8 @@ function registerOne(fastify, normalized, ctx) {
|
|
|
39
39
|
const authMw = buildAuthMiddleware(fastify, config.permissions);
|
|
40
40
|
const permissionFn = config.permissions;
|
|
41
41
|
const permissionMw = async (req, _reply) => {
|
|
42
|
-
const
|
|
42
|
+
const ctx = buildPermissionContextLite(req, normalized.name);
|
|
43
|
+
const raw = await permissionFn(ctx);
|
|
43
44
|
if (!normalizePermissionGranted(raw)) {
|
|
44
45
|
const status = req.user ? 403 : 401;
|
|
45
46
|
const reason = normalizePermissionReason(raw);
|