@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.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +37 -0
  3. package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-BnLYgE9A.mjs} +4 -4
  4. package/dist/{EventTransport-IEn7Tn-Q.d.mts → EventTransport-QmY7A258.d.mts} +0 -1
  5. package/dist/{QueryCache-TRfXau3W.d.mts → QueryCache-Z1B38koh.d.mts} +0 -1
  6. package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
  7. package/dist/{actionPermissions-D7NGwcAR.mjs → actionPermissions-ze70xXJi.mjs} +1 -1
  8. package/dist/audit/index.d.mts +9 -5
  9. package/dist/audit/index.mjs +1 -1
  10. package/dist/auth/audit.d.mts +7 -4
  11. package/dist/auth/audit.mjs +5 -0
  12. package/dist/auth/index.d.mts +7 -14
  13. package/dist/auth/index.mjs +7 -5
  14. package/dist/auth/redis-session.d.mts +1 -2
  15. package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
  16. package/dist/cache/index.d.mts +2 -3
  17. package/dist/cache/index.mjs +2 -2
  18. package/dist/cli/commands/describe.d.mts +1 -2
  19. package/dist/cli/commands/describe.mjs +2 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +1 -1
  22. package/dist/cli/commands/init.mjs +1 -1
  23. package/dist/cli/commands/introspect.mjs +4 -13
  24. package/dist/cli/index.mjs +2 -2
  25. package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
  26. package/dist/context/index.d.mts +2 -2
  27. package/dist/core/index.d.mts +3 -3
  28. package/dist/core/index.mjs +5 -5
  29. package/dist/{core-Ba-6Sue6.mjs → core-DZ6Xe1jR.mjs} +182 -55
  30. package/dist/{createActionRouter-B_WzBFyf.mjs → createActionRouter-BWMDW-dL.mjs} +4 -4
  31. package/dist/{createAggregationRouter-B_G_nIjm.mjs → createAggregationRouter-l5mNdYrZ.mjs} +5 -4
  32. package/dist/{createApp--703hQE1.mjs → createApp-Bi61XxqU.mjs} +229 -24
  33. package/dist/discovery/index.d.mts +0 -1
  34. package/dist/docs/index.d.mts +1 -2
  35. package/dist/docs/index.mjs +1 -1
  36. package/dist/{elevation-xGUBq6p_.d.mts → elevation-C9s5hFP_.d.mts} +0 -1
  37. package/dist/encryption/index.d.mts +2 -144
  38. package/dist/{errorHandler-BtUUDPEa.d.mts → errorHandler-B0e-bq-O.d.mts} +0 -1
  39. package/dist/{errors-DOHkjQlD.d.mts → errors-DHcHVAgk.d.mts} +0 -1
  40. package/dist/{eventPlugin-DbBFd3x_.d.mts → eventPlugin-A6EbthXR.d.mts} +10 -6
  41. package/dist/events/index.d.mts +9 -6
  42. package/dist/events/index.mjs +9 -6
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis.d.mts +1 -2
  45. package/dist/factory/index.d.mts +5 -4
  46. package/dist/factory/index.mjs +3 -3
  47. package/dist/{fastify-augmentation-D-5_yQJi.d.mts → fastify-augmentation-DafigGQp.d.mts} +12 -2
  48. package/dist/{fields-C7ahdGYf.d.mts → fields-BGqrAa_u.d.mts} +1 -2
  49. package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
  50. package/dist/hooks/index.d.mts +1 -1
  51. package/dist/idempotency/index.d.mts +4 -3
  52. package/dist/idempotency/index.mjs +1 -1
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/{index-CP6A1Ffj.d.mts → index-B5N-NaHs.d.mts} +88 -7
  55. package/dist/{index-CRKiiP5o.d.mts → index-B5PoNbfu.d.mts} +41 -7
  56. package/dist/{index-CUTG2Cuo.d.mts → index-Dtrj6XZk.d.mts} +90 -29
  57. package/dist/index.d.mts +7 -7
  58. package/dist/index.mjs +7 -7
  59. package/dist/{init-Bqpt1Hxf.mjs → init-0YhHkXGk.mjs} +239 -140
  60. package/dist/integrations/event-gateway.d.mts +2 -3
  61. package/dist/integrations/index.d.mts +2 -2
  62. package/dist/integrations/jobs.d.mts +0 -1
  63. package/dist/integrations/mcp/index.d.mts +6 -7
  64. package/dist/integrations/mcp/index.mjs +1 -1
  65. package/dist/integrations/mcp/testing.d.mts +1 -2
  66. package/dist/integrations/mcp/testing.mjs +1 -1
  67. package/dist/integrations/streamline.d.mts +19 -7
  68. package/dist/integrations/streamline.mjs +9 -5
  69. package/dist/integrations/webhooks.d.mts +0 -1
  70. package/dist/integrations/websocket-pushref-redis.d.mts +1 -2
  71. package/dist/integrations/websocket-redis.d.mts +1 -2
  72. package/dist/integrations/websocket.d.mts +2 -2
  73. package/dist/{loadResources-B8UmpITt.mjs → loadResources-DpU8pfjV.mjs} +4 -3
  74. package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
  75. package/dist/{mcpPlugin-C088Nd-G.mjs → mcpPlugin-B9XjjNVG.mjs} +148 -49
  76. package/dist/middleware/index.d.mts +1 -2
  77. package/dist/middleware/index.mjs +1 -1
  78. package/dist/module-WeuU5jfd.d.mts +429 -0
  79. package/dist/{openapi-DZ-5a5Op.mjs → openapi-Kyod_di6.mjs} +3 -3
  80. package/dist/permissions/index.d.mts +3 -3
  81. package/dist/permissions/index.mjs +2 -2
  82. package/dist/{permissions-BPB1xqGf.mjs → permissions-BJMuKMl7.mjs} +43 -3
  83. package/dist/pipeline/index.d.mts +1 -2
  84. package/dist/plugins/index.d.mts +87 -10
  85. package/dist/plugins/index.mjs +91 -4
  86. package/dist/plugins/response-cache.d.mts +5 -3
  87. package/dist/plugins/tracing-entry.d.mts +1 -1
  88. package/dist/plugins/tracing-entry.mjs +1 -1
  89. package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
  90. package/dist/presets/filesUpload.d.mts +4 -5
  91. package/dist/presets/filesUpload.mjs +1 -1
  92. package/dist/presets/index.d.mts +16 -3
  93. package/dist/presets/index.mjs +1 -1
  94. package/dist/presets/multiTenant.d.mts +1 -2
  95. package/dist/presets/multiTenant.mjs +1 -1
  96. package/dist/presets/search.d.mts +2 -3
  97. package/dist/presets/search.mjs +3 -2
  98. package/dist/{presets-BtyFwTO4.mjs → presets-gw0ciIUD.mjs} +1 -1
  99. package/dist/{pushref-registry-DnCqL3aa.d.mts → pushref-registry-BbcnpNTm.d.mts} +4 -2
  100. package/dist/{queryCachePlugin-xzFyKAjT.d.mts → queryCachePlugin-CfYWq-Ak.d.mts} +1 -2
  101. package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
  102. package/dist/{redis-C3W0qbz8.d.mts → redis-UDtLaNj6.d.mts} +0 -1
  103. package/dist/{redis-stream-DsP1vqZB.d.mts → redis-stream-iVR2l7aS.d.mts} +1 -2
  104. package/dist/{registerRoute-BdV8DDwU.mjs → registerRoute-oGAl0oA8.mjs} +1 -1
  105. package/dist/registry/index.d.mts +2 -2
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
  108. package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
  109. package/dist/schemas/index.d.mts +0 -1
  110. package/dist/scim/index.d.mts +0 -1
  111. package/dist/scim/index.mjs +2 -2
  112. package/dist/scope/index.d.mts +2 -3
  113. package/dist/{sessionManager-BXG-HGwG.d.mts → sessionManager-BHOM3N_0.d.mts} +10 -6
  114. package/dist/{storage-Co_PQpeQ.d.mts → storage-DLUZ04kK.d.mts} +8 -4
  115. package/dist/sync/changelog.d.mts +2 -0
  116. package/dist/sync/changelog.mjs +2 -0
  117. package/dist/testing/index.d.mts +2 -3
  118. package/dist/testing/index.mjs +6 -3
  119. package/dist/testing/storageContract.d.mts +1 -2
  120. package/dist/{tracing-D8jlGXP7.d.mts → tracing-CsGKQJGg.d.mts} +0 -1
  121. package/dist/types/index.d.mts +4 -4
  122. package/dist/types/storage.d.mts +1 -1
  123. package/dist/types-Cc91nXuA.d.mts +132 -0
  124. package/dist/{types-LNebCoJx.d.mts → types-DH0GupI7.d.mts} +8 -4
  125. package/dist/{types-DpvSF5Hh.d.mts → types-DRj-Ut4n.d.mts} +1 -2
  126. package/dist/{types-9_wtNPsI.d.mts → types-DpxoNX5v.d.mts} +112 -214
  127. package/dist/utils/index.d.mts +182 -128
  128. package/dist/utils/index.mjs +2 -2
  129. package/dist/{utils-C6QQ9-hd.mjs → utils-CO8a3B_1.mjs} +1435 -1391
  130. package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
  131. package/dist/{versioning-D_eBCfNr.d.mts → versioning-DQ4mCvg0.d.mts} +1 -2
  132. package/dist/{websocket-MlYlrYq0.d.mts → websocket-uuWqM6UY.d.mts} +5 -4
  133. package/package.json +18 -13
  134. package/skills/arc/SKILL.md +10 -0
  135. package/skills/arc/references/mcp.md +2 -0
  136. package/skills/arc-module-publishing/SKILL.md +242 -0
  137. package/skills/arc-module-publishing/references/authoring-modules.md +272 -0
  138. package/skills/arc-module-publishing/references/package-manifest.md +155 -0
  139. 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-gaEeMlz-.mjs";
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 { p as assertValidConfig, x as getDefaultCrudSchemas } from "./utils-C6QQ9-hd.mjs";
6
- import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-Dy6EQHjM.mjs";
7
- import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-BPB1xqGf.mjs";
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 pluralize } from "./pluralize-DccFV9R7.mjs";
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] ? deepMergeSchemas(schemas[key], converted) : converted;
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] = [...new Set([...result[key], ...value])];
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-B_WzBFyf.mjs").then((n) => n.n);
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-B_G_nIjm.mjs"), import("./validate-CRcaWzp2.mjs").then((n) => n.r)]);
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 { defineResource as a, createCrudRouter as c, defineResourceVariants as i, createPermissionMiddleware as l, getEntityIdField as n, ResourceDefinition as o, getEntityQuery as r, defineAction as s, getEntityId as t, defineAggregation as u };
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-gaEeMlz-.mjs";
2
+ import "./constants-DNre0wRg.mjs";
3
3
  import { f as createError } from "./errors-DkO8Nk6-.mjs";
4
- import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-D7NGwcAR.mjs";
5
- 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-BdV8DDwU.mjs";
6
- import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-D1MJwqmp.mjs";
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-BdV8DDwU.mjs";
3
- import { n as validateAggregations } from "./validate-CRcaWzp2.mjs";
4
- import { t as buildAggregationHandler } from "./buildHandler-B0qvAguf.mjs";
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 raw = await permissionFn(buildPermissionContextLite(req, normalized.name));
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);