@classytic/arc 2.25.0 → 2.30.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 (92) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController--M8EMzt0.mjs → BaseController-B5DwufbB.mjs} +15 -11
  3. package/dist/EventTransport-C6YA8xY9.mjs +103 -0
  4. package/dist/{HookSystem-C1vB_Gc9.mjs → HookSystem-WkIougo6.mjs} +15 -35
  5. package/dist/applyPermissionResult-Cf6zUCvb.mjs +257 -0
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +1 -1
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +1 -1
  10. package/dist/{buildHandler-Dtwd8fdz.mjs → buildHandler-BYS3Vkfz.mjs} +32 -6
  11. package/dist/cleanup/index.d.mts +313 -0
  12. package/dist/cleanup/index.mjs +1267 -0
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/docs.mjs +1 -1
  15. package/dist/core/index.d.mts +2 -2
  16. package/dist/core/index.mjs +5 -3
  17. package/dist/core-poalJLvp.mjs +175 -0
  18. package/dist/{createActionRouter-B6GvAuAX.mjs → createActionRouter-BxuywVRB.mjs} +2 -2
  19. package/dist/{createAggregationRouter-wzwFa_dW.mjs → createAggregationRouter-BiY-lGdY.mjs} +7 -20
  20. package/dist/{preloadResources-DfyngIN9.mjs → createApp-CXrKFciH.mjs} +25 -76
  21. package/dist/defineAggregation-ufWjUwml.mjs +47 -0
  22. package/dist/{core-EUNu0u07.mjs → defineResource-CxWqDCAy.mjs} +36 -261
  23. package/dist/docs/index.d.mts +1 -1
  24. package/dist/docs/index.mjs +1 -1
  25. package/dist/{eventPlugin-DrQ_FesK.mjs → eventPlugin-D2NNR4py.mjs} +2 -103
  26. package/dist/events/index.d.mts +28 -2
  27. package/dist/events/index.mjs +24 -343
  28. package/dist/factory/index.d.mts +4 -3
  29. package/dist/factory/index.mjs +3 -1
  30. package/dist/{fastify-augmentation-dPNBVeIj.d.mts → fastify-augmentation-0WhPnUH7.d.mts} +3 -2
  31. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → fields-CQ_7KQWw.mjs} +1 -97
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/hooks/index.mjs +1 -1
  34. package/dist/idempotency/index.mjs +1 -1
  35. package/dist/index-CCzVR-N3.d.mts +107 -0
  36. package/dist/{index-BMnbzvtO.d.mts → index-Ch16aAah.d.mts} +201 -21
  37. package/dist/{index-f4PTrk3B.d.mts → index-Cx7bro4R.d.mts} +4 -4
  38. package/dist/{index-DPRH0Cme.d.mts → index-Q5gzEIWY.d.mts} +97 -66
  39. package/dist/index.d.mts +6 -6
  40. package/dist/index.mjs +8 -6
  41. package/dist/integrations/index.d.mts +1 -1
  42. package/dist/integrations/mcp/index.d.mts +6 -6
  43. package/dist/integrations/mcp/index.mjs +1 -1
  44. package/dist/integrations/mcp/testing.d.mts +1 -1
  45. package/dist/integrations/mcp/testing.mjs +1 -1
  46. package/dist/{mcpPlugin-Cb7olw0j.mjs → mcpPlugin-nRzm5g2s.mjs} +36 -43
  47. package/dist/middleware/index.d.mts +1 -1
  48. package/dist/migrations/index.d.mts +0 -3
  49. package/dist/migrations/index.mjs +0 -3
  50. package/dist/module-_-M7C5ES.mjs +29 -0
  51. package/dist/{openapi-DrLtog_B.mjs → openapi-B8Vkbxbn.mjs} +1 -1
  52. package/dist/permissions/index.d.mts +3 -3
  53. package/dist/permissions/index.mjs +4 -3
  54. package/dist/{permissions-BrtiBKGs.mjs → permissions-Bfu_8y1U.mjs} +318 -289
  55. package/dist/pipeline/index.d.mts +1 -1
  56. package/dist/{pipeline-BN5G05rd.mjs → pipeline-BJnpkY9i.mjs} +4 -18
  57. package/dist/plugins/index.d.mts +4 -4
  58. package/dist/plugins/index.mjs +2 -2
  59. package/dist/plugins/tracing-entry.mjs +1 -1
  60. package/dist/{preloadResources-0oe-JSSS.d.mts → preloadResources-DCfa2egz.d.mts} +3 -2
  61. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  62. package/dist/presets/filesUpload.d.mts +2 -2
  63. package/dist/presets/filesUpload.mjs +1 -1
  64. package/dist/presets/index.d.mts +14 -2
  65. package/dist/presets/index.mjs +1 -1
  66. package/dist/presets/multiTenant.d.mts +1 -1
  67. package/dist/presets/search.d.mts +2 -2
  68. package/dist/presets/search.mjs +1 -1
  69. package/dist/{presets-DwFDVKP0.mjs → presets-CJOEXLuk.mjs} +25 -9
  70. package/dist/projection-UBDgfMwu.mjs +17 -0
  71. package/dist/{realtime-Cuw_deA-.mjs → realtime-B9nmIY5V.mjs} +2 -1
  72. package/dist/{registerRoute-v0Wb6Plt.mjs → registerRoute-JkxIEBJq.mjs} +3 -1
  73. package/dist/registry/index.d.mts +2 -2
  74. package/dist/registry/index.mjs +1 -1
  75. package/dist/{registry-vcOCtPij.mjs → registry-BYXUUg21.mjs} +16 -1
  76. package/dist/repositoryFilter-Dz8k3S4P.mjs +44 -0
  77. package/dist/subscribe-helpers-DfzsivM6.mjs +339 -0
  78. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  79. package/dist/testing/cleanupStoreContract.mjs +375 -0
  80. package/dist/testing/index.d.mts +34 -5
  81. package/dist/testing/index.mjs +316 -211
  82. package/dist/testing/outboxStoreContract.d.mts +13 -0
  83. package/dist/testing/outboxStoreContract.mjs +247 -0
  84. package/dist/types/index.d.mts +3 -3
  85. package/dist/{types-QFGc3Cjv.d.mts → types-Bmmn-fO0.d.mts} +1 -1
  86. package/dist/types-C_ZZRcD6.d.mts +450 -0
  87. package/dist/{types-Dmw-6Qeo.d.mts → types-ChvvVNP8.d.mts} +93 -88
  88. package/dist/{index-KUoxjIq1.d.mts → types-D_n8L6VY.d.mts} +33 -106
  89. package/dist/usage/index.d.mts +1 -1
  90. package/dist/usage/index.mjs +0 -0
  91. package/dist/utils/index.d.mts +2 -5
  92. package/package.json +539 -524
@@ -3,17 +3,17 @@ import { arcLog } from "./logger/index.mjs";
3
3
  import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
5
5
  import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-CSyCxl2V.mjs";
6
- import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController--M8EMzt0.mjs";
6
+ import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-B5DwufbB.mjs";
7
7
  import { a as buildRouteHooks, c as buildAuthMiddleware, i as selectPluginMw, n as buildRouteConfig, o as resolveRoutePreHandlers, r as resolveRouterPluginMw, s as routeHookOptions, t as buildRateLimitConfig, u as buildArcDecorator } from "./rateLimit-DrED-C4v.mjs";
8
- import { a as applyFieldWritePermissions, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
9
- import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-v0Wb6Plt.mjs";
8
+ import { i as resolveEffectiveRoles, n as applyFieldWritePermissions } from "./fields-CQ_7KQWw.mjs";
9
+ import { r as buildCrudPermissionMw, t as tryRegisterRoute } from "./registerRoute-JkxIEBJq.mjs";
10
10
  import { c as getDefaultCrudSchemas } from "./utils-taUVD4p3.mjs";
11
11
  import { n as convertRouteSchema, r as ensureSchemaConverter, t as convertOpenApiSchemas } from "./schemaConverter-DJvodiHj.mjs";
12
12
  import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
13
13
  import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
14
- import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BN5G05rd.mjs";
15
- import { A as requireAuth } from "./permissions-BrtiBKGs.mjs";
16
- import { r as getAvailablePresets, t as applyPresets } from "./presets-DwFDVKP0.mjs";
14
+ import { a as createFastifyHandler, i as createCrudHandlers, n as buildPipelineHandler, o as createRequestContext, r as resolvePipelineSteps } from "./pipeline-BJnpkY9i.mjs";
15
+ import { I as requireAuth } from "./permissions-Bfu_8y1U.mjs";
16
+ import { r as getAvailablePresets, t as applyPresets } from "./presets-CJOEXLuk.mjs";
17
17
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
18
18
  import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
19
19
  //#region src/core/middlewares/fieldWrite.ts
@@ -55,52 +55,6 @@ function methodCarriesBody(method) {
55
55
  return m === "POST" || m === "PUT" || m === "PATCH";
56
56
  }
57
57
  //#endregion
58
- //#region src/core/aggregation/defineAggregation.ts
59
- /**
60
- * Declare a single resource aggregation. Exported configs flow into
61
- * `defineResource({ aggregations: { ... } })` either as named keys or
62
- * via auto-discovery patterns (loadAggregations — future).
63
- *
64
- * @example Inline
65
- * ```ts
66
- * defineResource({
67
- * name: 'order',
68
- * aggregations: {
69
- * revenueByStatus: defineAggregation({
70
- * groupBy: 'status',
71
- * measures: { count: 'count', revenue: 'sum:totalPrice' },
72
- * permissions: requireRoles(['admin']),
73
- * }),
74
- * },
75
- * });
76
- * ```
77
- *
78
- * @example Multi-file
79
- * ```ts
80
- * // orders/aggregations/revenue-by-status.ts
81
- * import { defineAggregation } from '@classytic/arc';
82
- *
83
- * export const revenueByStatus = defineAggregation({
84
- * groupBy: 'status',
85
- * measures: { count: 'count', revenue: 'sum:totalPrice' },
86
- * permissions: requireRoles(['admin']),
87
- * timeout: 5000,
88
- * cache: { staleTime: 60 },
89
- * });
90
- *
91
- * // orders/order.resource.ts
92
- * import * as aggregations from './aggregations/index.js';
93
- *
94
- * defineResource({
95
- * name: 'order',
96
- * aggregations, // 30+ entries flow in
97
- * });
98
- * ```
99
- */
100
- function defineAggregation(config) {
101
- return config;
102
- }
103
- //#endregion
104
58
  //#region src/core/createCrudRouter.ts
105
59
  /**
106
60
  * Mount custom routes (from presets or user-defined `routes`) on Fastify.
@@ -340,80 +294,6 @@ function createPermissionMiddleware(permission, resourceName, action) {
340
294
  return buildCrudPermissionMw(permission, resourceName, action);
341
295
  }
342
296
  //#endregion
343
- //#region src/core/defineAction.ts
344
- /**
345
- * Build an `ActionDefinition` with a typed handler. The literal schema
346
- * type captured here flows into `data`, so `defineAction({ schema:
347
- * z.object({...}), handler })` produces fully-typed code with no
348
- * `as MyShape` cast.
349
- *
350
- * Behaviorally identical to a bare `ActionDefinition` object — same
351
- * validation path (AJV), same permission resolution, same MCP wiring.
352
- * The runtime shape is unchanged; only the type-level inference is new.
353
- */
354
- function defineAction(config) {
355
- return {
356
- handler: config.handler,
357
- permissions: config.permissions,
358
- schema: config.schema,
359
- description: config.description,
360
- id: config.id,
361
- mcp: config.mcp
362
- };
363
- }
364
- //#endregion
365
- //#region src/core/defineResource/seams.ts
366
- /** Plain object test — literals only; class instances must replace, not merge. */
367
- function isPlainObject(value) {
368
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
369
- const proto = Object.getPrototypeOf(value);
370
- return proto === Object.prototype || proto === null;
371
- }
372
- /** Recursive plain-object merge; nested arrays REPLACE (they're value lists). */
373
- function mergePlain(base, next) {
374
- const out = { ...base };
375
- for (const [key, value] of Object.entries(next)) {
376
- if (value === void 0) continue;
377
- const existing = out[key];
378
- out[key] = isPlainObject(existing) && isPlainObject(value) ? mergePlain(existing, value) : value;
379
- }
380
- return out;
381
- }
382
- /**
383
- * Canonical slot-aware merge of a base resource config with host seams.
384
- * Later seams win over earlier ones; `undefined` values never clobber.
385
- *
386
- * Merge semantics (value-driven — no per-slot registry to drift):
387
- *
388
- * - **Top-level arrays CONCAT** — `routes`, `middlewares`, `presets`,
389
- * `routeGuards`, `disabledRoutes`: additive collections, base first.
390
- * - **Plain objects merge recursively** — `permissions`, `actions`,
391
- * `schemaOptions` (incl. `fieldRules`), `aggregations`, `customSchemas`,
392
- * `fields`, `cache`, `events`: per-key composition. Arrays NESTED inside
393
- * them replace (they're value lists — enums, sort specs), not concat.
394
- * - **Everything else last-wins** — class instances (`adapter`,
395
- * `controller`, `queryParser`), functions, scalars.
396
- *
397
- * The single `as` at the return is the one documented erasure for the
398
- * {@link AdapterLike}-widened seam slots — inside the helper so no module
399
- * author ever writes one (mirrors mongokit's one-cast-at-the-boundary rule).
400
- */
401
- function mergeResourceConfig(base, ...seams) {
402
- const out = { ...base };
403
- for (const seam of seams) {
404
- if (!seam) continue;
405
- for (const [key, value] of Object.entries(seam)) {
406
- if (value === void 0) continue;
407
- const existing = out[key];
408
- if (Array.isArray(existing) && Array.isArray(value)) out[key] = [...existing, ...value];
409
- else if (isPlainObject(existing) && isPlainObject(value)) out[key] = mergePlain(existing, value);
410
- else if (Array.isArray(value)) out[key] = [...value];
411
- else out[key] = value;
412
- }
413
- }
414
- return out;
415
- }
416
- //#endregion
417
297
  //#region src/core/defineResource/controller.ts
418
298
  /**
419
299
  * Phase 4 — pick (or auto-create) the resource's controller.
@@ -582,26 +462,6 @@ function buildBaseController(resolvedConfig, adapter, repository) {
582
462
  //#endregion
583
463
  //#region src/core/defineResource/hooks.ts
584
464
  /**
585
- * Phase 6 — wire preset hooks + inline `config.hooks` onto the
586
- * resource's `_pendingHooks`.
587
- *
588
- * Two sources feed the same array:
589
- * 1. Preset hooks collected during `applyPresets()` (raw `_hooks`
590
- * on the internal config). Already in the canonical
591
- * `{ operation, phase, handler, priority }` shape — copied
592
- * verbatim (priority defaults to 10).
593
- * 2. Inline `config.hooks.beforeCreate` / `afterCreate` / etc.
594
- * Authored by the user on the original `ResourceConfig`.
595
- * Wrapped in a `ResourceHookContext` projection (v2.10.8) so
596
- * authors can read `scope` / `context` without reaching into
597
- * internal request fields.
598
- *
599
- * The 6 inline hook keys (before/after × create/update/delete) used
600
- * to be 6 nearly-identical blocks; collapsed into a table + loop so
601
- * a future hook (e.g. `beforeRead`) is one row, not seven scattered
602
- * edits.
603
- */
604
- /**
605
465
  * Combined entry-point for Phase 6. Pushes preset-collected hooks
606
466
  * first, then inline `config.hooks` (so hosts can rely on
607
467
  * registration order if priorities tie).
@@ -664,19 +524,17 @@ const INLINE_HOOK_SPECS = [
664
524
  }
665
525
  ];
666
526
  /**
667
- * Project a raw HookSystem context into a `ResourceHookContext` for
668
- * inline `config.hooks.*` handlers. The projection lifts `scope`
669
- * out of `context._scope` so authors don't reach into internal
670
- * fields.
527
+ * Project a raw HookSystem context into a `ResourceHookContext` for inline
528
+ * `config.hooks.*` handlers. `scope` rides in already computed — `HookContext`
529
+ * carries the same projection — so the two surfaces cannot drift.
671
530
  */
672
531
  function buildHookContext(ctx) {
673
532
  const context = ctx.context;
674
- const rawScope = context?._scope;
675
533
  return {
676
534
  data: ctx.data ?? ctx.result ?? {},
677
535
  user: ctx.user,
678
536
  context,
679
- scope: buildRequestScopeProjection(rawScope),
537
+ scope: ctx.scope,
680
538
  meta: ctx.meta
681
539
  };
682
540
  }
@@ -922,7 +780,7 @@ function stripFromRequired(schema, fieldName) {
922
780
  }
923
781
  /**
924
782
  * Strip framework-injected fields from the `required[]` list of every
925
- * body-shaped slot in an adapter's generated schemas (v2.11.0).
783
+ * body-shaped slot in an adapter's generated schemas.
926
784
  *
927
785
  * A "framework-injected field" is any field marked `systemManaged: true`
928
786
  * in `schemaOptions.fieldRules`. Arc populates those fields from the
@@ -1077,22 +935,32 @@ const WRITE_OPERATIONS = /* @__PURE__ */ new Set([
1077
935
  * wiring, ...) count as gates and don't false-positive.
1078
936
  *
1079
937
  * Severity ladder:
1080
- * - `warn` when any enabled WRITE op (create/update/delete) is ungated —
1081
- * this is real exposure the host must acknowledge.
938
+ * - `error` when any enabled WRITE op is ungated AND `strict` is on — the
939
+ * resource fails boot (`defineResource()` throws). This is the fail-closed
940
+ * production invariant: an unauthenticated write must never ship silently.
941
+ * - `warn` (default) when any enabled WRITE op (create/update/delete) is
942
+ * ungated — real exposure the host must acknowledge, but non-fatal so
943
+ * existing hosts are not broken.
1082
944
  * - `info` when only reads are ungated — public catalogs
1083
945
  * (`referenceData: true`) are a legitimate shape, but the host should
1084
946
  * still state intent with `permissions.fullPublic()` / `allowPublic()`.
947
+ *
948
+ * `strict` is opt-in (resource `strictPermissions: true` or the
949
+ * `ARC_STRICT_PERMISSIONS` env), so the default behavior is unchanged.
1085
950
  */
1086
- function collectUngatedCrudDiagnostics(config) {
951
+ function collectUngatedCrudDiagnostics(config, strict = false) {
1087
952
  if (config.disableDefaultRoutes) return [];
1088
953
  const disabled = new Set(config.disabledRoutes ?? []);
1089
954
  const permissions = config.permissions ?? {};
1090
955
  const ungated = CRUD_OPERATIONS.filter((op) => !disabled.has(op) && typeof permissions[op] !== "function");
1091
956
  if (ungated.length === 0) return [];
957
+ const severity = ungated.some((op) => WRITE_OPERATIONS.has(op)) ? strict ? "error" : "warn" : "info";
1092
958
  return [{
1093
- severity: ungated.some((op) => WRITE_OPERATIONS.has(op)) ? "warn" : "info",
959
+ severity,
1094
960
  code: "crud-public-by-omission",
1095
- 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`
961
+ message: `[Arc] Resource '${config.name}' mounts ${ungated.length} CRUD route(s) with no permission gate: ${ungated.join(", ")}.\n` + (severity === "error" ? "strictPermissions is on and an ungated WRITE is fatal. State intent explicitly:\n" : "An omitted permission mounts the route WITHOUT auth. State intent explicitly:\n") + ` permissions: permissions.fullPublic() // intentionally public
962
+ permissions: permissions.authenticated() // require auth on every op
963
+ permissions: { ${ungated[0]}: requireAuth(), ... } // per-operation`
1096
964
  }];
1097
965
  }
1098
966
  //#endregion
@@ -1337,7 +1205,7 @@ function buildResourcePlugin(resource) {
1337
1205
  const isFirstMount = !resource._sharedStateRegisteredOn.has(sharedRoot);
1338
1206
  if (isFirstMount) resource._sharedStateRegisteredOn.add(sharedRoot);
1339
1207
  if (isFirstMount && resource._diagnostics?.length) for (const diagnostic of resource._diagnostics) {
1340
- const level = diagnostic.severity === "info" ? "info" : "warn";
1208
+ const level = diagnostic.severity === "info" ? "info" : diagnostic.severity === "error" ? "error" : "warn";
1341
1209
  fastify.log?.[level]?.(diagnostic.message);
1342
1210
  }
1343
1211
  const arc = fastify.arc;
@@ -1398,7 +1266,7 @@ function buildResourcePlugin(resource) {
1398
1266
  extensions: resource.extensions
1399
1267
  });
1400
1268
  if (resource.actions && Object.keys(resource.actions).length > 0) {
1401
- const { createActionRouter } = await import("./createActionRouter-B6GvAuAX.mjs").then((n) => n.n);
1269
+ const { createActionRouter } = await import("./createActionRouter-BxuywVRB.mjs").then((n) => n.n);
1402
1270
  createActionRouter(typedInstance, {
1403
1271
  ...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
1404
1272
  resourceName: resource.name,
@@ -1413,7 +1281,7 @@ function buildResourcePlugin(resource) {
1413
1281
  });
1414
1282
  }
1415
1283
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1416
- const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-wzwFa_dW.mjs"), import("./validate-Bk44F2yT.mjs").then((n) => n.r)]);
1284
+ const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-BiY-lGdY.mjs"), import("./validate-Bk44F2yT.mjs").then((n) => n.r)]);
1417
1285
  const repoForAgg = resource.controller?.repository ?? resource.repository;
1418
1286
  if (!adapterSupportsAggregate(repoForAgg)) {
1419
1287
  const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
@@ -2289,7 +2157,12 @@ function defineResource(config) {
2289
2157
  const configWithId = resolveIdField(normalisedConfig, repository);
2290
2158
  const resolvedConfig = applyPresetsAndAutoInject(configWithId);
2291
2159
  const hasCrudRoutes = computeHasCrudRoutes(resolvedConfig);
2292
- if (!normalisedConfig.skipValidation) diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig));
2160
+ if (!normalisedConfig.skipValidation) {
2161
+ const strict = resolvedConfig.strictPermissions ?? process.env.ARC_STRICT_PERMISSIONS === "true";
2162
+ diagnostics = diagnostics.concat(collectUngatedCrudDiagnostics(resolvedConfig, strict));
2163
+ }
2164
+ const fatal = diagnostics.filter((d) => d.severity === "error");
2165
+ if (fatal.length > 0) throw new Error(fatal.map((d) => d.message).join("\n\n"));
2293
2166
  const narrowedConfig = resolvedConfig;
2294
2167
  const narrowedAdapter = configWithId.adapter;
2295
2168
  const controller = resolveOrAutoCreateController(narrowedConfig, narrowedAdapter, repository, hasCrudRoutes);
@@ -2308,102 +2181,4 @@ function defineResource(config) {
2308
2181
  return resource;
2309
2182
  }
2310
2183
  //#endregion
2311
- //#region src/core/defineResourceVariants.ts
2312
- /**
2313
- * Define multiple resources from a shared base config and per-variant overrides.
2314
- *
2315
- * Each variant is independently passed through `defineResource()` — the
2316
- * returned `ResourceDefinition`s are real, fully-registered resources.
2317
- * Register each one's plugin in your app:
2318
- *
2319
- * ```typescript
2320
- * await app.register(articlePublic.toPlugin());
2321
- * await app.register(articleAdmin.toPlugin());
2322
- * ```
2323
- *
2324
- * @param base Shared config — adapter, queryParser, schemaOptions, hooks, etc.
2325
- * Must NOT include `name` or `prefix` (those are per-variant).
2326
- * @param variants Map of variant key → override. Each variant must declare
2327
- * its own `name` and `prefix`. Other fields override the base.
2328
- * @returns A record where each key from `variants` maps to a real
2329
- * `ResourceDefinition` ready for `.toPlugin()` registration.
2330
- */
2331
- function defineResourceVariants(base, variants) {
2332
- const out = {};
2333
- for (const key of Object.keys(variants)) {
2334
- const override = variants[key];
2335
- out[key] = defineResource({
2336
- ...base,
2337
- ...override
2338
- });
2339
- }
2340
- return out;
2341
- }
2342
- //#endregion
2343
- //#region src/core/entityHelpers.ts
2344
- /**
2345
- * Per-request entity helpers — read the resource binding (idField + the
2346
- * URL `:id` value) off `req.arc`.
2347
- *
2348
- * Action handlers receive their `id` argument as the raw URL `:id` value.
2349
- * When the resource declares a custom `idField` (`slug`, `reportId`, …)
2350
- * that's NOT the document `_id`, a naive `Model.findById(id)` silently
2351
- * returns null. The historical footgun was a `findById(id)` typo where
2352
- * the handler author hadn't realised that `:id` resolves to the friendly
2353
- * handle.
2354
- *
2355
- * `getEntityQuery(req)` produces the canonical filter shape so handlers
2356
- * compose lookups with no resource-config recall:
2357
- *
2358
- * ```ts
2359
- * actions: {
2360
- * archive: {
2361
- * handler: async (id, data, req) => {
2362
- * const doc = await Model.findOne(getEntityQuery(req));
2363
- * if (!doc) throw new NotFoundError("Order");
2364
- * // ...
2365
- * },
2366
- * },
2367
- * }
2368
- * ```
2369
- *
2370
- * The router populates `req.arc.idField` and `req.arc.entityId` before
2371
- * invoking the handler — these helpers are zero-cost reads.
2372
- */
2373
- /**
2374
- * Read the resource's configured `idField` for the current request.
2375
- * Falls back to the framework default (`_id`) when the route hasn't
2376
- * bound an idField — keeps handlers safe to author without checking
2377
- * the resource config.
2378
- */
2379
- function getEntityIdField(req) {
2380
- return req.arc?.idField ?? "_id";
2381
- }
2382
- /**
2383
- * Read the URL `:id` path param value as the resource handle.
2384
- * Returns `undefined` when the route has no `:id` segment (collection
2385
- * routes) — handlers that need the entity must be on row routes.
2386
- */
2387
- function getEntityId(req) {
2388
- if (req.arc?.entityId !== void 0) return req.arc.entityId;
2389
- return req.params?.id;
2390
- }
2391
- /**
2392
- * Compose a `findOne` filter that resolves the current request's
2393
- * entity, regardless of whether the resource binds `_id` or a custom
2394
- * field. Idiomatic shape for arc action handlers.
2395
- *
2396
- * ```ts
2397
- * const doc = await Model.findOne(getEntityQuery(req));
2398
- * ```
2399
- *
2400
- * Returns `{}` when the route has no entity context (collection routes
2401
- * or tests bypassing the router) — caller decides what that means.
2402
- */
2403
- function getEntityQuery(req) {
2404
- const id = getEntityId(req);
2405
- if (id === void 0) return {};
2406
- return { [getEntityIdField(req)]: id };
2407
- }
2408
- //#endregion
2409
- export { defineResource as a, validateResourceConfig as c, defineAction as d, createCrudRouter as f, defineResourceVariants as i, ResourceDefinition as l, defineAggregation as m, getEntityIdField as n, assertValidConfig as o, createPermissionMiddleware as p, getEntityQuery as r, formatValidationErrors as s, getEntityId as t, mergeResourceConfig as u };
2184
+ export { ResourceDefinition as a, validateResourceConfig as i, assertValidConfig as n, createCrudRouter as o, formatValidationErrors as r, createPermissionMiddleware as s, defineResource as t };
@@ -1,4 +1,4 @@
1
- import { p as RegistryEntry } from "../index-DPRH0Cme.mjs";
1
+ import { p as RegistryEntry } from "../index-Q5gzEIWY.mjs";
2
2
  import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  //#region src/docs/openapi/types.d.ts
@@ -1,5 +1,5 @@
1
1
  import { n as getUserRoles } from "../userHelpers-Cg_y3fBz.mjs";
2
- import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-DrLtog_B.mjs";
2
+ import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-B8Vkbxbn.mjs";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/docs/scalar.ts
5
5
  const scalarPlugin = async (fastify, opts = {}) => {
@@ -2,109 +2,8 @@ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { arcLog } from "./logger/index.mjs";
3
3
  import { d as createDomainError } from "./errors-DkO8Nk6-.mjs";
4
4
  import { n as requestContext } from "./requestContext-MrlJwECf.mjs";
5
+ import { n as createEvent, t as MemoryEventTransport } from "./EventTransport-C6YA8xY9.mjs";
5
6
  import fp from "fastify-plugin";
6
- import { createEvent, matchEventPattern } from "@classytic/primitives/events";
7
- //#region src/events/EventTransport.ts
8
- /**
9
- * Internal arc events module — concrete in-memory transport.
10
- *
11
- * **Public type contracts moved out (arc 2.12).** `EventMeta`, `DomainEvent`,
12
- * `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`,
13
- * `PublishManyResult`, plus the `createEvent` / `createChildEvent` /
14
- * `matchEventPattern` helpers, are now owned by `@classytic/primitives/events`.
15
- * Two reasons:
16
- *
17
- * 1. The shapes ARE the cross-package contract. arc, arc-next, mongokit's
18
- * audit-log plugin, future kits and downstream services all share them.
19
- * Owning them in `primitives` (pure types, zero runtime, zero deps)
20
- * eliminates the "manually mirror in two places" problem the previous
21
- * `primitives/src/events.ts` header explicitly called out.
22
- * 2. Inverting the dependency direction lets future packages (sqlitekit
23
- * audit, billing services) consume the contract without depending on
24
- * arc's HTTP-coupled stack.
25
- *
26
- * Hosts MUST import the types from primitives directly:
27
- *
28
- * ```typescript
29
- * import type {
30
- * EventMeta, DomainEvent, EventHandler, EventTransport,
31
- * DeadLetteredEvent, PublishManyResult,
32
- * } from '@classytic/primitives/events';
33
- * import { createEvent, createChildEvent } from '@classytic/primitives/events';
34
- * ```
35
- *
36
- * arc's `events/index.ts` barrel does NOT re-export these — by design, so
37
- * that consumer code is forced into the canonical import path and the org's
38
- * "no re-exports" rule holds at every public surface.
39
- *
40
- * What stays here:
41
- * - `MemoryEventTransport` — the in-memory `EventTransport` implementation
42
- * used as arc's default transport. Its handler-set state is process-local;
43
- * wrong layer for primitives.
44
- * - `MemoryEventTransportOptions` — its options shape.
45
- *
46
- * Inside arc, files that previously imported types from `./EventTransport.js`
47
- * keep working: this file re-exports them from primitives for arc's own
48
- * call sites. That re-export is an internal refactor convenience — the
49
- * public `events/index.ts` barrel stays clean.
50
- */
51
- /**
52
- * In-memory event transport (default).
53
- *
54
- * Events are delivered synchronously within the process. Not suitable for
55
- * multi-instance deployments — pair with `RedisEventTransport` /
56
- * `RedisStreamTransport` (subpath imports) for those.
57
- */
58
- var MemoryEventTransport = class {
59
- name = "memory";
60
- handlers = /* @__PURE__ */ new Map();
61
- logger;
62
- constructor(options) {
63
- this.logger = options?.logger ?? console;
64
- }
65
- async publish(event) {
66
- const allHandlers = /* @__PURE__ */ new Set();
67
- for (const [pattern, handlers] of this.handlers) if (matchEventPattern(pattern, event.type)) for (const h of handlers) allHandlers.add(h);
68
- for (const handler of allHandlers) try {
69
- await handler(event);
70
- } catch (err) {
71
- this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
72
- }
73
- }
74
- /**
75
- * Reference `publishMany` implementation — delegates to `publish()` in order.
76
- *
77
- * Production transports (Kafka, Redis pipeline, SQS batch) should override
78
- * this with a single batched network call. Memory transport has nothing to
79
- * batch, so we just loop — the loop still returns a proper result map so
80
- * `EventOutbox.relay` can exercise the batched code path in tests.
81
- */
82
- async publishMany(events) {
83
- const results = /* @__PURE__ */ new Map();
84
- for (const event of events) try {
85
- await this.publish(event);
86
- results.set(event.meta.id, null);
87
- } catch (err) {
88
- results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
89
- }
90
- return results;
91
- }
92
- async subscribe(pattern, handler) {
93
- if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
94
- this.handlers.get(pattern)?.add(handler);
95
- return () => {
96
- const set = this.handlers.get(pattern);
97
- if (set) {
98
- set.delete(handler);
99
- if (set.size === 0) this.handlers.delete(pattern);
100
- }
101
- };
102
- }
103
- async close() {
104
- this.handlers.clear();
105
- }
106
- };
107
- //#endregion
108
7
  //#region src/events/retry.ts
109
8
  /**
110
9
  * Wrap an event handler with retry logic and dead letter support.
@@ -338,4 +237,4 @@ var eventPlugin_default = fp(eventPlugin, {
338
237
  fastify: "5.x"
339
238
  });
340
239
  //#endregion
341
- export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createEvent as o, createDeadLetterPublisher as r, eventPlugin as t };
240
+ export { withRetry as i, eventPlugin_exports as n, createDeadLetterPublisher as r, eventPlugin as t };
@@ -72,6 +72,16 @@ interface MemoryEntry {
72
72
  declare class MemoryOutboxStore implements OutboxStore {
73
73
  private readonly entries;
74
74
  private readonly seenDedupeKeys;
75
+ /**
76
+ * Drop every entry (including delivered / failed / dead-lettered rows) and
77
+ * the dedupe-key memory.
78
+ *
79
+ * For tests only — it is the in-memory analogue of truncating the table, and
80
+ * exists so a suite can isolate cases against ONE store instance (the
81
+ * `reset` hook of `runOutboxStoreContract`). Not part of `OutboxStore`; a
82
+ * production store must never expose this.
83
+ */
84
+ clear(): void;
75
85
  save(event: DomainEvent, options?: OutboxWriteOptions): Promise<void>;
76
86
  getPending(limit: number): Promise<DomainEvent[]>;
77
87
  claimPending(options?: OutboxClaimOptions): Promise<DomainEvent[]>;
@@ -395,7 +405,23 @@ declare class EventOutbox {
395
405
  }
396
406
  //#endregion
397
407
  //#region src/events/repository-outbox-adapter.d.ts
398
- declare function repositoryAsOutboxStore(repository: RepositoryLike): OutboxStore;
408
+ interface RepositoryOutboxStoreOptions {
409
+ /**
410
+ * Column holding the "not claimable before" timestamp. Default
411
+ * `'visibleAt'`.
412
+ *
413
+ * Exists so a host with an EXISTING outbox table can adopt this adapter
414
+ * without renaming a column and rebuilding its claim index — the one thing
415
+ * that otherwise forces a migration on the table that guarantees delivery.
416
+ * A store using `nextVisibleAt` (with a `{status, nextVisibleAt, createdAt}`
417
+ * index) passes `{ visibleAtField: 'nextVisibleAt' }` and keeps both.
418
+ *
419
+ * The field is used in the claimable filter, on `save`, and on `fail`'s
420
+ * retry scheduling — all three stay in lockstep by construction.
421
+ */
422
+ readonly visibleAtField?: string;
423
+ }
424
+ declare function repositoryAsOutboxStore(repository: RepositoryLike, options?: RepositoryOutboxStoreOptions): OutboxStore;
399
425
  //#endregion
400
426
  //#region src/events/subscribe-helpers.d.ts
401
427
  /**
@@ -556,4 +582,4 @@ interface FastifyEventBus {
556
582
  };
557
583
  }
558
584
  //#endregion
559
- export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type PayloadOf, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };
585
+ export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type CustomValidator, type EventDefinitionInput, type EventDefinitionOutput, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventRegistryOptions, type EventSchema, type ExponentialBackoffOptions, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxRelayErrorHandler, type OutboxRelayErrorKind, type PayloadOf, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RelayResult, type RepositoryOutboxStoreOptions, type RetryOptions, type ValidationResult, type WrapWithBoundaryOptions, type WrapWithSchemaOptions, createDeadLetterPublisher, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, subscribeWithBoundary, subscribeWithSchema, withRetry, wrapWithBoundary, wrapWithSchema };