@classytic/arc 2.19.0 → 2.21.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-BnLYgE9A.mjs} +4 -4
- package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
- package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
- package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
- package/dist/{actionPermissions-D7NGwcAR.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
- package/dist/audit/index.d.mts +9 -5
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/audit.d.mts +7 -4
- package/dist/auth/audit.mjs +5 -0
- package/dist/auth/index.d.mts +7 -14
- package/dist/auth/index.mjs +7 -5
- package/dist/auth/redis-session.d.mts +1 -2
- package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
- package/dist/cache/index.d.mts +2 -3
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -2
- 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/context/index.d.mts +2 -2
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +5 -5
- package/dist/{core-Ba-6Sue6.mjs → core-DZ6Xe1jR.mjs} +182 -55
- package/dist/{createActionRouter-B_WzBFyf.mjs → createActionRouter-BWMDW-dL.mjs} +4 -4
- package/dist/{createAggregationRouter-B_G_nIjm.mjs → createAggregationRouter-l5mNdYrZ.mjs} +5 -4
- package/dist/{createApp--703hQE1.mjs → createApp-Bi61XxqU.mjs} +229 -24
- package/dist/discovery/index.d.mts +0 -1
- package/dist/docs/index.d.mts +1 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
- package/dist/encryption/index.d.mts +2 -144
- package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-B0e-bq-O.d.mts} +0 -1
- package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
- package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
- package/dist/events/index.d.mts +9 -6
- package/dist/events/index.mjs +9 -6
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -2
- package/dist/factory/index.d.mts +5 -4
- package/dist/factory/index.mjs +3 -3
- package/dist/{fastify-augmentation-D-5_yQJi.d.mts → fastify-augmentation-DafigGQp.d.mts} +12 -2
- package/dist/{fields-C7ahdGYf.d.mts → fields-BGqrAa_u.d.mts} +1 -2
- package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +4 -3
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-CP6A1Ffj.d.mts → index-B5N-NaHs.d.mts} +88 -7
- package/dist/{index-CRKiiP5o.d.mts → index-B5PoNbfu.d.mts} +41 -7
- package/dist/{index-CUTG2Cuo.d.mts → index-Dtrj6XZk.d.mts} +90 -29
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{init-Bqpt1Hxf.mjs → init-0YhHkXGk.mjs} +239 -140
- package/dist/integrations/event-gateway.d.mts +2 -3
- package/dist/integrations/index.d.mts +2 -2
- package/dist/integrations/jobs.d.mts +0 -1
- package/dist/integrations/mcp/index.d.mts +6 -7
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -2
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +19 -7
- package/dist/integrations/streamline.mjs +9 -5
- package/dist/integrations/webhooks.d.mts +0 -1
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
- package/dist/integrations/websocket-redis.d.mts +1 -2
- package/dist/integrations/websocket.d.mts +2 -2
- package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
- package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
- package/dist/{mcpPlugin-C088Nd-G.mjs → mcpPlugin-B9XjjNVG.mjs} +148 -49
- package/dist/middleware/index.d.mts +1 -2
- package/dist/middleware/index.mjs +1 -1
- package/dist/module-WeuU5jfd.d.mts +429 -0
- package/dist/{openapi-DZ-5a5Op.mjs → openapi-Kyod_di6.mjs} +3 -3
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-BPB1xqGf.mjs → permissions-BJMuKMl7.mjs} +43 -3
- package/dist/pipeline/index.d.mts +1 -2
- package/dist/plugins/index.d.mts +87 -10
- package/dist/plugins/index.mjs +91 -4
- package/dist/plugins/response-cache.d.mts +5 -3
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
- package/dist/presets/filesUpload.d.mts +4 -5
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +16 -3
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -2
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -3
- package/dist/presets/search.mjs +3 -2
- package/dist/{presets-BtyFwTO4.mjs → presets-gw0ciIUD.mjs} +1 -1
- package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
- package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
- package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
- package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
- package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
- package/dist/{registerRoute-BdV8DDwU.mjs → registerRoute-oGAl0oA8.mjs} +1 -1
- package/dist/registry/index.d.mts +2 -2
- 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/schemas/index.d.mts +0 -1
- package/dist/scim/index.d.mts +0 -1
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +2 -3
- package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
- package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
- package/dist/sync/changelog.d.mts +2 -0
- package/dist/sync/changelog.mjs +2 -0
- package/dist/testing/index.d.mts +2 -3
- package/dist/testing/index.mjs +6 -3
- package/dist/testing/storageContract.d.mts +1 -2
- package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/storage.d.mts +1 -1
- package/dist/types-Cc91nXuA.d.mts +132 -0
- package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
- package/dist/{types-DpvSF5Hh.d.mts → types-DRj-Ut4n.d.mts} +1 -2
- package/dist/{types-9_wtNPsI.d.mts → types-DpxoNX5v.d.mts} +112 -214
- package/dist/utils/index.d.mts +182 -128
- package/dist/utils/index.mjs +2 -2
- package/dist/{utils-C6QQ9-hd.mjs → utils-CO8a3B_1.mjs} +1435 -1391
- package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
- package/dist/{versioning-D_eBCfNr.d.mts → versioning-DQ4mCvg0.d.mts} +1 -2
- package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
- package/package.json +18 -13
- package/skills/arc/SKILL.md +10 -0
- package/skills/arc/references/mcp.md +2 -0
- package/skills/arc-module-publishing/SKILL.md +242 -0
- package/skills/arc-module-publishing/references/authoring-modules.md +272 -0
- package/skills/arc-module-publishing/references/package-manifest.md +155 -0
- package/skills/arc-module-publishing/references/publishing-workflow.md +182 -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 { M as assertValidConfig, l as getDefaultCrudSchemas } from "./utils-CO8a3B_1.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-BnLYgE9A.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-oGAl0oA8.mjs";
|
|
8
|
+
import { I as applyFieldWritePermissions, R as resolveEffectiveRoles } from "./permissions-BJMuKMl7.mjs";
|
|
8
9
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
10
|
+
import { t as applyPresets } from "./presets-gw0ciIUD.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-D7NGwcAR.mjs";
|
|
12
|
-
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-BdV8DDwU.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-ze70xXJi.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,45 +98,6 @@ 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.
|
|
@@ -177,7 +177,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
177
177
|
}
|
|
178
178
|
return result;
|
|
179
179
|
} : handler,
|
|
180
|
-
...buildRouteConfig(rateLimitConfig, extensions)
|
|
180
|
+
...buildRouteConfig(route.rateLimit !== void 0 ? buildRateLimitConfig(route.rateLimit) : rateLimitConfig, extensions)
|
|
181
181
|
}, {
|
|
182
182
|
resourceName,
|
|
183
183
|
op: opName
|
|
@@ -949,6 +949,42 @@ function computeHasCrudRoutes(config) {
|
|
|
949
949
|
const disabled = new Set(config.disabledRoutes ?? []);
|
|
950
950
|
return !config.disableDefaultRoutes && CRUD_OPERATIONS.some((op) => !disabled.has(op));
|
|
951
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
|
+
}
|
|
952
988
|
//#endregion
|
|
953
989
|
//#region src/registry/resolveTenantPurge.ts
|
|
954
990
|
const DEFAULT_PRIORITY = 100;
|
|
@@ -1032,7 +1068,10 @@ function buildGeneratedCrudSchemas(openApi, customSchemas) {
|
|
|
1032
1068
|
for (const [op, customSchema] of Object.entries(customSchemas)) {
|
|
1033
1069
|
const key = op;
|
|
1034
1070
|
const converted = convertRouteSchema(customSchema);
|
|
1035
|
-
schemas[key] = schemas[key] ?
|
|
1071
|
+
schemas[key] = schemas[key] ? {
|
|
1072
|
+
...schemas[key],
|
|
1073
|
+
...converted
|
|
1074
|
+
} : converted;
|
|
1036
1075
|
}
|
|
1037
1076
|
}
|
|
1038
1077
|
return schemas;
|
|
@@ -1102,7 +1141,7 @@ function deepMergeSchemas(base, override) {
|
|
|
1102
1141
|
if (!override) return base;
|
|
1103
1142
|
if (!base) return override;
|
|
1104
1143
|
const result = { ...base };
|
|
1105
|
-
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [
|
|
1144
|
+
for (const [key, value] of Object.entries(override)) if (Array.isArray(value) && Array.isArray(result[key])) result[key] = [.../* @__PURE__ */ new Set([...result[key], ...value])];
|
|
1106
1145
|
else if (value && typeof value === "object" && !Array.isArray(value)) result[key] = deepMergeSchemas(result[key], value);
|
|
1107
1146
|
else result[key] = value;
|
|
1108
1147
|
return result;
|
|
@@ -1247,7 +1286,7 @@ function buildResourcePlugin(resource) {
|
|
|
1247
1286
|
extensions: resource.extensions
|
|
1248
1287
|
});
|
|
1249
1288
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1250
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1289
|
+
const { createActionRouter } = await import("./createActionRouter-BWMDW-dL.mjs").then((n) => n.n);
|
|
1251
1290
|
createActionRouter(typedInstance, {
|
|
1252
1291
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1253
1292
|
resourceName: resource.name,
|
|
@@ -1262,7 +1301,7 @@ function buildResourcePlugin(resource) {
|
|
|
1262
1301
|
});
|
|
1263
1302
|
}
|
|
1264
1303
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1265
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1304
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-l5mNdYrZ.mjs"), import("./validate-C2mGgSXg.mjs").then((n) => n.r)]);
|
|
1266
1305
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1267
1306
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1268
1307
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1718,7 +1757,7 @@ function mergeUserOpenApiOverrides(openApiSchemas, userOverrides) {
|
|
|
1718
1757
|
* call) since the set is fixed and the cost of re-allocating is a
|
|
1719
1758
|
* pointless boot tax for hosts with hundreds of resources.
|
|
1720
1759
|
*/
|
|
1721
|
-
const CRUD_OP_NAMES = new Set([
|
|
1760
|
+
const CRUD_OP_NAMES = /* @__PURE__ */ new Set([
|
|
1722
1761
|
"create",
|
|
1723
1762
|
"update",
|
|
1724
1763
|
"delete",
|
|
@@ -1738,7 +1777,42 @@ function validateDefineResourceConfig(config) {
|
|
|
1738
1777
|
validatePermissionsShape(config);
|
|
1739
1778
|
validateCustomRoutePermissions(config);
|
|
1740
1779
|
validateActionsShape(config);
|
|
1741
|
-
return collectRedundantFieldRuleDiagnostics(config);
|
|
1780
|
+
return [...collectRedundantFieldRuleDiagnostics(config), ...collectReservedFilterNameDiagnostics(config)];
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Query-string param names arc's `QueryParser` reserves for
|
|
1784
|
+
* pagination/projection (`q.after ?? q.cursor` is the keyset cursor). A
|
|
1785
|
+
* document field with one of these names can never be filtered via the query
|
|
1786
|
+
* string — the parser consumes the name before it reaches the filter map — and
|
|
1787
|
+
* the same reservation flows into the MCP list-tool schema. Silent pre-2.20;
|
|
1788
|
+
* now surfaced.
|
|
1789
|
+
*/
|
|
1790
|
+
const RESERVED_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
1791
|
+
"page",
|
|
1792
|
+
"limit",
|
|
1793
|
+
"cursor",
|
|
1794
|
+
"after",
|
|
1795
|
+
"sort",
|
|
1796
|
+
"search",
|
|
1797
|
+
"select",
|
|
1798
|
+
"populate"
|
|
1799
|
+
]);
|
|
1800
|
+
/**
|
|
1801
|
+
* Warn when a declared filterable field's NAME collides with a reserved query
|
|
1802
|
+
* param. Verified empirically (tests/e2e/reserved-field-name-collision.test.ts):
|
|
1803
|
+
* a field named `cursor`/`page` is silently unfilterable. Boot diagnostic, not
|
|
1804
|
+
* a hard error — the resource still works, the field just isn't query-filterable.
|
|
1805
|
+
*/
|
|
1806
|
+
function collectReservedFilterNameDiagnostics(config) {
|
|
1807
|
+
const filterable = config.schemaOptions?.filterableFields;
|
|
1808
|
+
if (!filterable) return [];
|
|
1809
|
+
const collisions = filterable.filter((f) => RESERVED_QUERY_PARAMS.has(f));
|
|
1810
|
+
if (collisions.length === 0) return [];
|
|
1811
|
+
return [{
|
|
1812
|
+
severity: "warn",
|
|
1813
|
+
code: "filter-field-reserved-name",
|
|
1814
|
+
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.`
|
|
1815
|
+
}];
|
|
1742
1816
|
}
|
|
1743
1817
|
/** Permissions must be `PermissionCheck` functions, not arbitrary values. */
|
|
1744
1818
|
function validatePermissionsShape(config) {
|
|
@@ -1820,6 +1894,7 @@ function defineResource(config) {
|
|
|
1820
1894
|
const configWithId = resolveIdField(normalisedConfig, repository);
|
|
1821
1895
|
const resolvedConfig = applyPresetsAndAutoInject(configWithId);
|
|
1822
1896
|
const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
|
|
1897
|
+
if (!normalisedConfig.skipValidation) diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig));
|
|
1823
1898
|
const narrowedConfig = resolvedConfig;
|
|
1824
1899
|
const narrowedAdapter = configWithId.adapter;
|
|
1825
1900
|
const controller = resolveOrAutoCreateController(narrowedConfig, narrowedAdapter, repository, hasCrudRoutes);
|
|
@@ -1838,6 +1913,58 @@ function defineResource(config) {
|
|
|
1838
1913
|
return resource;
|
|
1839
1914
|
}
|
|
1840
1915
|
//#endregion
|
|
1916
|
+
//#region src/core/defineResource/seams.ts
|
|
1917
|
+
/** Plain object test — literals only; class instances must replace, not merge. */
|
|
1918
|
+
function isPlainObject(value) {
|
|
1919
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1920
|
+
const proto = Object.getPrototypeOf(value);
|
|
1921
|
+
return proto === Object.prototype || proto === null;
|
|
1922
|
+
}
|
|
1923
|
+
/** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
|
|
1924
|
+
function mergePlain(base, next) {
|
|
1925
|
+
const out = { ...base };
|
|
1926
|
+
for (const [key, value] of Object.entries(next)) {
|
|
1927
|
+
if (value === void 0) continue;
|
|
1928
|
+
const existing = out[key];
|
|
1929
|
+
out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
|
|
1930
|
+
}
|
|
1931
|
+
return out;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Canonical slot-aware merge of a base resource config with host seams.
|
|
1935
|
+
* Later seams win over earlier ones; `undefined` values never clobber.
|
|
1936
|
+
*
|
|
1937
|
+
* Merge semantics (value-driven — no per-slot registry to drift):
|
|
1938
|
+
*
|
|
1939
|
+
* - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
|
|
1940
|
+
* `routeGuards`, `disabledRoutes`: additive collections, base first.
|
|
1941
|
+
* - **Plain objects merge recursively** — `permissions`, `actions`,
|
|
1942
|
+
* `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
|
|
1943
|
+
* `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
|
|
1944
|
+
* them replace (they're value lists — enums, sort specs), not concat.
|
|
1945
|
+
* - **Everything else last-wins** — class instances (`adapter`,
|
|
1946
|
+
* `controller`, `queryParser`), functions, scalars.
|
|
1947
|
+
*
|
|
1948
|
+
* The single `as` at the return is the one documented erasure for the
|
|
1949
|
+
* {@link AdapterLike}-widened seam slots — inside the helper so no module
|
|
1950
|
+
* author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
|
|
1951
|
+
*/
|
|
1952
|
+
function mergeResourceConfig(base, ...seams) {
|
|
1953
|
+
const out = { ...base };
|
|
1954
|
+
for (const seam of seams) {
|
|
1955
|
+
if (!seam) continue;
|
|
1956
|
+
for (const [key, value] of Object.entries(seam)) {
|
|
1957
|
+
if (value === void 0) continue;
|
|
1958
|
+
const existing = out[key];
|
|
1959
|
+
if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
|
|
1960
|
+
else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
|
|
1961
|
+
else if (Array.isArray(value)) out[key] = [...value];
|
|
1962
|
+
else out[key] = value;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}
|
|
1967
|
+
//#endregion
|
|
1841
1968
|
//#region src/core/defineResourceVariants.ts
|
|
1842
1969
|
/**
|
|
1843
1970
|
* Define multiple resources from a shared base config and per-variant overrides.
|
|
@@ -1936,4 +2063,4 @@ function getEntityQuery(req) {
|
|
|
1936
2063
|
return { [getEntityIdField(req)]: id };
|
|
1937
2064
|
}
|
|
1938
2065
|
//#endregion
|
|
1939
|
-
export {
|
|
2066
|
+
export { mergeResourceConfig as a, defineAction as c, defineAggregation as d, defineResourceVariants as i, createCrudRouter as l, getEntityIdField as n, defineResource as o, getEntityQuery as r, ResourceDefinition as s, getEntityId as t, createPermissionMiddleware as u };
|
|
@@ -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-oGAl0oA8.mjs";
|
|
5
|
+
import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-ze70xXJi.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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-
|
|
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-oGAl0oA8.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);
|