@classytic/arc 2.33.0 → 2.35.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 (184) hide show
  1. package/dist/{BaseController-CaQC5EXX.mjs → BaseController-C-pofd0v.mjs} +131 -16
  2. package/dist/{EventTransport-tq4CEB-q.mjs → EventTransport-BrPeGUil.mjs} +1 -1
  3. package/dist/{HookSystem-DqXkMXkB.mjs → HookSystem-LlPm3Anm.mjs} +6 -4
  4. package/dist/{ResourceRegistry-DoALUlOO.mjs → ResourceRegistry-yuD1Ra53.mjs} +2 -2
  5. package/dist/{pipeline-q69DZzgG.mjs → actionPermissions-LFuOra7W.mjs} +30 -7
  6. package/dist/audit/index.d.mts +2 -2
  7. package/dist/audit/index.mjs +2 -2
  8. package/dist/auth/audit.mjs +1 -1
  9. package/dist/auth/index.d.mts +2 -2
  10. package/dist/auth/index.mjs +6 -6
  11. package/dist/auth/redis-session.mjs +1 -1
  12. package/dist/{authorizationDecision-B3mP_QTy.mjs → authorizationDecision-DYXBBgtg.mjs} +6 -2
  13. package/dist/{base-Csjc2oF-.d.mts → base-GZU3LlzP.d.mts} +1 -1
  14. package/dist/{buildHandler-DLE9xX-V.mjs → buildHandler-Do9aMIvL.mjs} +4 -4
  15. package/dist/cache/index.mjs +4 -4
  16. package/dist/{caching-BcGmvnHh.mjs → caching-cN5X9VEL.mjs} +2 -2
  17. package/dist/cleanup/index.d.mts +2 -2
  18. package/dist/cleanup/index.mjs +4 -4
  19. package/dist/cli/commands/describe.d.mts +1 -1
  20. package/dist/cli/commands/describe.mjs +3 -3
  21. package/dist/cli/commands/docs.mjs +3 -3
  22. package/dist/cli/commands/doctor.mjs +1 -1
  23. package/dist/cli/commands/generate.mjs +2 -2
  24. package/dist/cli/commands/init.mjs +2 -2
  25. package/dist/cli/commands/introspect.mjs +2 -2
  26. package/dist/cli/index.mjs +3 -3
  27. package/dist/{constants-BxCoxmiN.mjs → constants-BhzLXCfN.mjs} +1 -1
  28. package/dist/context/index.d.mts +16 -1
  29. package/dist/context/index.mjs +64 -4
  30. package/dist/core/index.d.mts +3 -3
  31. package/dist/core/index.mjs +8 -8
  32. package/dist/{core-dxhcHhUD.mjs → core-DdEvmmkY.mjs} +3 -3
  33. package/dist/{createActionRouter-DVpoA0do.mjs → createActionRouter-EYWgd0ly.mjs} +27 -64
  34. package/dist/{createAggregationRouter-B_B48JZ-.mjs → createAggregationRouter-ByOK9Iev.mjs} +7 -7
  35. package/dist/defineAggregation-DoIijIFq.mjs +25 -0
  36. package/dist/{defineResource-DR9vsUYy.mjs → defineResource-BOrGeocE.mjs} +62 -66
  37. package/dist/discovery/index.mjs +1 -1
  38. package/dist/docs/index.d.mts +1 -1
  39. package/dist/docs/index.mjs +3 -3
  40. package/dist/{elevation-B0tlY8y1.mjs → elevation-tGIAldco.mjs} +4 -4
  41. package/dist/encryption/index.mjs +3 -3
  42. package/dist/{errorHandler-C7xBqHsI.mjs → errorHandler-BbQ1nQdk.mjs} +3 -3
  43. package/dist/{errors-Cd_yy7aW.mjs → errors-CLid6atr.mjs} +1 -1
  44. package/dist/{eventPlugin-DSNEZWfV.d.mts → eventPlugin-B47sfCJ8.d.mts} +1 -1
  45. package/dist/{eventPlugin-Dar8qRaj.mjs → eventPlugin-ClPn5kjn.mjs} +5 -5
  46. package/dist/events/index.d.mts +2 -2
  47. package/dist/events/index.mjs +5 -5
  48. package/dist/events/transports/redis-stream-entry.mjs +1 -1
  49. package/dist/events/transports/redis.mjs +1 -1
  50. package/dist/factory/index.d.mts +5 -5
  51. package/dist/factory/index.mjs +5 -6
  52. package/dist/{fastify-augmentation-DVsTAy6X.d.mts → fastify-augmentation-CMMTL3cj.d.mts} +3 -3
  53. package/dist/{fields-CojeUkS4.mjs → fields-UtpiJO5R.mjs} +1 -1
  54. package/dist/{generate-DiB0nHPR.mjs → generate-BoAVy3_b.mjs} +2 -2
  55. package/dist/{headers-CveNO9gR.mjs → headers-DODWf7aC.mjs} +2 -2
  56. package/dist/{health-CasG2A28.mjs → health-_Zs3R_Sb.mjs} +1 -1
  57. package/dist/hooks/index.d.mts +1 -1
  58. package/dist/hooks/index.mjs +2 -2
  59. package/dist/idempotency/index.mjs +23 -58
  60. package/dist/idempotency/redis.mjs +1 -1
  61. package/dist/{index-C5NnMEKF.d.mts → index-C50BUxKs.d.mts} +1 -1
  62. package/dist/{index-BcR2bFZQ.d.mts → index-C5K78SXo.d.mts} +33 -105
  63. package/dist/{index-ugr1tQlW.d.mts → index-CJCcbADG.d.mts} +1 -1
  64. package/dist/{index-B-DvUzsr.d.mts → index-K7auNNW0.d.mts} +2 -2
  65. package/dist/{index-Brin0ODU.d.mts → index-h-mTgMqy.d.mts} +21 -31
  66. package/dist/index.d.mts +6 -6
  67. package/dist/index.mjs +12 -12
  68. package/dist/{init-mdJ-v4Xp.mjs → init-Ce4R0ZGK.mjs} +1 -1
  69. package/dist/integrations/event-gateway.mjs +2 -2
  70. package/dist/integrations/index.d.mts +2 -2
  71. package/dist/integrations/index.mjs +1 -1
  72. package/dist/integrations/jobs/index.d.mts +1 -1
  73. package/dist/integrations/jobs/index.mjs +2 -2
  74. package/dist/integrations/mcp/index.d.mts +4 -4
  75. package/dist/integrations/mcp/index.mjs +2 -2
  76. package/dist/integrations/mcp/testing.d.mts +1 -1
  77. package/dist/integrations/mcp/testing.mjs +3 -3
  78. package/dist/integrations/streamline.mjs +3 -3
  79. package/dist/integrations/webhooks.d.mts +1 -1
  80. package/dist/integrations/webhooks.mjs +11 -24
  81. package/dist/integrations/websocket-pushref-redis.mjs +1 -1
  82. package/dist/integrations/websocket-redis.mjs +1 -1
  83. package/dist/integrations/websocket.mjs +1 -1
  84. package/dist/{interface-DznJrm9R.mjs → interface-my7sxjw5.mjs} +1 -1
  85. package/dist/{jsonBody-CfGH-GxO.mjs → jsonBody-CVKA7C5m.mjs} +2 -2
  86. package/dist/{keys-WeKvY1dE.mjs → keys-GlU7XpmG.mjs} +1 -1
  87. package/dist/{loadResources-Bj-aSsXi.mjs → loadResources-DdQb0qgV.mjs} +2 -2
  88. package/dist/{loadResourcesFromEntry-DwaxNcE2.mjs → loadResourcesFromEntry-s6aGs6-0.mjs} +2 -2
  89. package/dist/logger/index.d.mts +23 -4
  90. package/dist/logger/index.mjs +50 -7
  91. package/dist/{mcpPlugin-aJg5TqHi.mjs → mcpPlugin-DkzZ7Nfx.mjs} +15 -16
  92. package/dist/{memory-qYSB7rMQ.mjs → memory-BwedEXau.mjs} +2 -2
  93. package/dist/{metrics-D7uchLjN.mjs → metrics-5LNPvL3P.mjs} +2 -2
  94. package/dist/middleware/index.d.mts +1 -1
  95. package/dist/middleware/index.mjs +3 -3
  96. package/dist/migrations/index.mjs +7 -4
  97. package/dist/{module-CwKsgZXp.mjs → module-Bh5OC7ww.mjs} +1 -1
  98. package/dist/{multipartBody-4w-S3-PU.mjs → multipartBody-DjilSDAT.mjs} +1 -1
  99. package/dist/{openapi-BN5cQaO2.mjs → openapi-ItLH12_L.mjs} +6 -6
  100. package/dist/outbox-admin/index.d.mts +2 -2
  101. package/dist/outbox-admin/index.mjs +4 -4
  102. package/dist/{outbox-module-CXB79bvt.mjs → outbox-module-SGIZz6oS.mjs} +14 -5
  103. package/dist/{outbox-module-D-VFq37T.d.mts → outbox-module-b1TyDGGi.d.mts} +40 -3
  104. package/dist/permissions/index.d.mts +2 -2
  105. package/dist/permissions/index.mjs +5 -5
  106. package/dist/{permissions-BAfwgBxc.mjs → permissions-CpSBlFXZ.mjs} +6 -6
  107. package/dist/{pipe-BzzoEhHj.mjs → pipe-DNOuCfAs.mjs} +2 -2
  108. package/dist/pipeline/index.d.mts +1 -1
  109. package/dist/pipeline/index.mjs +2 -2
  110. package/dist/plugins/index.d.mts +92 -7
  111. package/dist/plugins/index.mjs +168 -17
  112. package/dist/plugins/response-cache.mjs +2 -2
  113. package/dist/plugins/tracing-entry.mjs +2 -2
  114. package/dist/{pluralize-B7xf8qZF.mjs → pluralize-DGAQQKL8.mjs} +1 -1
  115. package/dist/{preloadResources-g07ldtzh.d.mts → preloadResources--ZtY9pe9.d.mts} +54 -270
  116. package/dist/{createApp-CLGegqiG.mjs → preloadResources-DYMzmUk1.mjs} +112 -31
  117. package/dist/presets/filesUpload.d.mts +3 -3
  118. package/dist/presets/filesUpload.mjs +5 -5
  119. package/dist/presets/index.d.mts +2 -2
  120. package/dist/presets/index.mjs +2 -2
  121. package/dist/presets/multiTenant.d.mts +1 -1
  122. package/dist/presets/multiTenant.mjs +3 -3
  123. package/dist/presets/search.d.mts +9 -47
  124. package/dist/presets/search.mjs +2 -2
  125. package/dist/{presets-D-h0uWtP.mjs → presets-Bi0Z6Iq8.mjs} +4 -4
  126. package/dist/{projection-BKM0Ui-s.mjs → projection-aevMua7Q.mjs} +2 -2
  127. package/dist/{queryCachePlugin-DhsZ0ZIc.mjs → queryCachePlugin-BI7Z88Cx.mjs} +5 -5
  128. package/dist/{rateLimitKey-XngqVNEV.mjs → rateLimitKey-pyevMUVf.mjs} +1 -1
  129. package/dist/{realtime-CTkv3O59.mjs → realtime-Cfl2bVjj.mjs} +9 -9
  130. package/dist/{rateLimit-D4-ASwsI.mjs → registerRoute-u1opU66a.mjs} +114 -3
  131. package/dist/registry/index.d.mts +2 -2
  132. package/dist/registry/index.mjs +3 -3
  133. package/dist/{registry-CLxCR5LF.mjs → registry-B4sf9Rfm.mjs} +15 -56
  134. package/dist/{renewingLease-BnWtbA2U.mjs → renewingLease-5ESPMxp0.mjs} +14 -2
  135. package/dist/{replyHelpers-BSBVCwL3.mjs → replyHelpers-2jfmMnnV.mjs} +2 -2
  136. package/dist/{requestContext-uYE35JxI.mjs → requestContext-DzOUGNdY.mjs} +1 -1
  137. package/dist/{rolldown-runtime-DNg-KNCk.mjs → rolldown-runtime-CEvvVXTC.mjs} +1 -1
  138. package/dist/{runtimeCapabilities-Dm88DaTW.mjs → runtimeCapabilities-BagWc3TI.mjs} +1 -1
  139. package/dist/{schedules-CAcZgia0.mjs → schedules-BLpZLWGF.mjs} +6 -4
  140. package/dist/{schemaConverter-XH1RZRyu.mjs → schemaConverter-D2_bHGil.mjs} +19 -32
  141. package/dist/{schemaIR-CDvclAew.mjs → schemaIR-s3-ouZmP.mjs} +2 -2
  142. package/dist/schemas/index.mjs +1 -1
  143. package/dist/scim/index.mjs +2 -2
  144. package/dist/scope/index.d.mts +1 -1
  145. package/dist/scope/index.mjs +5 -5
  146. package/dist/{scope-BaWcqBvr.mjs → scope-DgepeCv6.mjs} +4 -4
  147. package/dist/simpleEqualityMatcher-BO-sFTQf.mjs +43 -0
  148. package/dist/{sse-Cf0jUHsh.mjs → sse-DUIP1XH6.mjs} +5 -5
  149. package/dist/{sseStream-Bvpi-UXQ.mjs → sseStream-ByFGyIVr.mjs} +2 -2
  150. package/dist/{store-helpers-B88p99Z2.mjs → store-helpers-kIJCY0wI.mjs} +1 -1
  151. package/dist/{streaming-DsT1B5zJ.mjs → streaming-DOpZVht6.mjs} +1 -1
  152. package/dist/{subscribe-helpers-CLIOHsOK.mjs → subscribe-helpers-Dh2BnzJf.mjs} +12 -46
  153. package/dist/sync/changelog.mjs +1 -1
  154. package/dist/testing/cleanupStoreContract.mjs +1 -1
  155. package/dist/testing/index.d.mts +6 -6
  156. package/dist/testing/index.mjs +10 -11
  157. package/dist/testing/outboxStoreContract.mjs +1 -1
  158. package/dist/testing/storageContract.mjs +1 -1
  159. package/dist/{transactionContext-BYtWnwAS.mjs → transactionContext-BTZzEcRo.mjs} +1 -1
  160. package/dist/{typeGuards-jjKJDN13.mjs → typeGuards-B-_jqaMi.mjs} +1 -1
  161. package/dist/types/index.d.mts +3 -3
  162. package/dist/types/index.mjs +1 -1
  163. package/dist/types/storage.mjs +1 -1
  164. package/dist/{types-BcTumAz_.d.mts → types-DDD5RKTu.d.mts} +30 -87
  165. package/dist/{types-BfC83KsP.d.mts → types-DSvHH0uR.d.mts} +1 -1
  166. package/dist/{types-ChuShpCP.d.mts → types-Ddl9OjmE.d.mts} +18 -121
  167. package/dist/{types-CoqIRrkY.mjs → types-GY4kATJb.mjs} +10 -53
  168. package/dist/usage/index.d.mts +2 -2
  169. package/dist/usage/index.mjs +0 -0
  170. package/dist/{userHelpers-CWF5pbZD.mjs → userHelpers-AKcB4V03.mjs} +1 -1
  171. package/dist/utils/index.d.mts +16 -42
  172. package/dist/utils/index.mjs +11 -11
  173. package/dist/{utils-h1V_sT-t.mjs → utils-CBK9HQOI.mjs} +3 -3
  174. package/dist/{validate-CdQlz52F.mjs → validate-dosAIvQU.mjs} +2 -2
  175. package/dist/{versioning-rZbyLVD-.mjs → versioning-BbTs4pwM.mjs} +3 -3
  176. package/package.json +8 -8
  177. package/skills/arc/SKILL.md +9 -9
  178. package/skills/arc/references/api-reference.md +1 -1
  179. package/dist/actionPermissions-CUeCmpUd.mjs +0 -25
  180. package/dist/defineAggregation-Nv-DwfOf.mjs +0 -48
  181. package/dist/preloadResources-BkyqeTDz.mjs +0 -46
  182. package/dist/registerRoute-DNnd3vkz.mjs +0 -113
  183. package/dist/repositoryFilter-DKny7PDn.mjs +0 -45
  184. package/dist/simpleEqualityMatcher-Se8VrsaP.mjs +0 -69
@@ -69,13 +69,13 @@ await app.listen({ port: 8040, host: '0.0.0.0' });
69
69
 
70
70
  **Boot order (fixed):** `beforeBoot` → modules resolve → `plugins` → `bootstrap[]` → `resources` → `afterResources` → `onReady`.
71
71
 
72
- **`role` — one image, N deployments (2.34).** `createApp({ role })` declares what a process is FOR, so the same codebase ships as api / worker / relay / scheduler without per-deployment builds:
72
+ **`role` — one image, N deployments (2.33).** `createApp({ role })` declares what a process is FOR, so the same codebase ships as api / worker / relay / scheduler without per-deployment builds:
73
73
 
74
74
  ```typescript
75
75
  createApp({ role: process.env.ARC_ROLE as 'api' | 'worker' | 'relay' | 'scheduler' | 'all' })
76
76
  ```
77
77
 
78
- `api` and `worker` mount NO module schedule arms; `relay` mounts only `kind: 'relay'` arms (the outbox relay, durable webhook delivery); `scheduler` mounts all of them. Declaring `relay`/`scheduler` together with `resources`/`resourceDir` is BOOT-FATAL — a relay process must not serve routes. Default `all` is the pre-2.34 behaviour, unchanged. `createWorker()` forwards `role`; without one it still runs everything non-HTTP.
78
+ `api` and `worker` mount NO module schedule arms; `relay` mounts only `kind: 'relay'` arms (the outbox relay, durable webhook delivery); `scheduler` mounts all of them. Declaring `relay`/`scheduler` together with `resources`/`resourceDir` is BOOT-FATAL — a relay process must not serve routes. Default `all` is the pre-2.33 behaviour, unchanged. `createWorker()` forwards `role`; without one it still runs everything non-HTTP.
79
79
 
80
80
  `beforeBoot()` (2.21) runs before Fastify exists and before module thunks / resource files import — connect the DB there instead of dynamic-import ordering hacks.
81
81
 
@@ -321,9 +321,9 @@ Decorates `app.authenticate` / `app.optionalAuthenticate` / `app.authorize`.
321
321
  - **`verifySignature(body, …)`** throws on parsed body — pass `req.rawBody`.
322
322
  - **MCP tools fail-closed.** A resource action without per-action perm + no `actionPermissions` + no `permissions.update` fallback → throws at tool generation. Declare `allowPublic()` to opt into unauthenticated.
323
323
  - **Better Auth roles.** BA stores `member.role = "admin,recruiter"` (comma-separated). Arc splits into `scope.orgRoles = ['admin', 'recruiter']`; `?role=admin` won't match — use `role[like]=admin`.
324
- - **Read-only repositories refuse write ROUTES at boot (2.34).** Kits' Better Auth overlays are sealed by default (`capabilities.readOnly`) — BA owns hashing, session revocation, org cascades. Mounting `create`/`update`/`delete` over one is boot-fatal: disable those routes, or pass the kit's `unsafeWritable: true` for administrative repair only. Normal identity mutations go through `auth.api`.
325
- - **Arc closes only stores/transports it BUILT (2.34).** One you hand to `idempotencyPlugin` / `eventPlugin` / `queryCachePlugin` stays yours to close — arc used to close every one, which wiped a co-resident app's live idempotency records.
326
- - **Idempotency self-checks its store at boot (2.34).** A probe write must read back under its own key and NOT under another; registration throws otherwise. Catches a filter-stripping repository (a pathless Mongoose schema under `strictQuery: true` strips filters to `{}`), which otherwise replays cached responses across keys AND users. `selfCheck: false` to disable; the adapter's per-read identity checks remain.
324
+ - **Read-only repositories refuse write ROUTES at boot (2.33).** Kits' Better Auth overlays are sealed by default (`capabilities.readOnly`) — BA owns hashing, session revocation, org cascades. Mounting `create`/`update`/`delete` over one is boot-fatal: disable those routes, or pass the kit's `unsafeWritable: true` for administrative repair only. Normal identity mutations go through `auth.api`.
325
+ - **Arc closes only stores/transports it BUILT (2.33).** One you hand to `idempotencyPlugin` / `eventPlugin` / `queryCachePlugin` stays yours to close — arc used to close every one, which wiped a co-resident app's live idempotency records.
326
+ - **Idempotency self-checks its store at boot (2.33).** A probe write must read back under its own key and NOT under another; registration throws otherwise. Catches a filter-stripping repository (a pathless Mongoose schema under `strictQuery: true` strips filters to `{}`), which otherwise replays cached responses across keys AND users. `selfCheck: false` to disable; the adapter's per-read identity checks remain.
327
327
 
328
328
  ## routeGuards + defineGuard
329
329
 
@@ -489,7 +489,7 @@ defineResource({
489
489
 
490
490
  POST/PATCH/DELETE bumps resource version. Response header: `x-cache: HIT | STALE | MISS`. `runtime: 'distributed'` requires `stores.queryCache: RedisCacheStore`.
491
491
 
492
- ## Transactional writes (2.34)
492
+ ## Transactional writes (2.33)
493
493
 
494
494
  `transactional: true` runs the PERSISTENCE step inside `withTransaction`. Write verbs receive the tx-bound repository plus `ctx.uow` — the join point for work outside the repository:
495
495
 
@@ -532,9 +532,9 @@ createApp({ modules: [createOutboxModule({ store: repositoryAsOutboxStore(outbox
532
532
  // Dev: createOutboxModule({ store: new MemoryOutboxStore() })
533
533
  ```
534
534
 
535
- The relay arm is `kind: 'relay'`, so `role: 'relay'` deployments drain it. It resolves the RAW transport, never the `fastify.events` facade (the facade fails open, which would acknowledge rows whose publish failed) — no transport is boot-fatal. **Fencing (2.34):** stores that support `claimPendingFenced` mint a token in the claim CAS; a relay that lost its lease mid-batch cannot ack or re-schedule a row a successor already owns. Feature-detected end to end.
535
+ The relay arm is `kind: 'relay'`, so `role: 'relay'` deployments drain it. It resolves the RAW transport, never the `fastify.events` facade (the facade fails open, which would acknowledge rows whose publish failed) — no transport is boot-fatal. **Fencing (2.33):** stores that support `claimPendingFenced` mint a token in the claim CAS; a relay that lost its lease mid-batch cannot ack or re-schedule a row a successor already owns. Feature-detected end to end.
536
536
 
537
- **Durable webhooks (2.34)** — delivery survives crash and deploy, using the outbox machinery rather than a second queue:
537
+ **Durable webhooks (2.33)** — delivery survives crash and deploy, using the outbox machinery rather than a second queue:
538
538
 
539
539
  ```typescript
540
540
  const store = repositoryAsOutboxStore(deliveryRepo);
@@ -727,7 +727,7 @@ await app.audit.custom('order', req.params.id, 'refund', { reason }, { user });
727
727
 
728
728
  `@classytic/arc/usage` — `usagePlugin` decorates `fastify.usage` with per-actor, per-UTC-month counters (`record`/`summary`/`period`/`actorOf`; actor chain org → user → client → ip). Auto-tracks `api.requests` (default on) + `api.egress.bytes` (opt-in); recording is fail-safe (a throwing `UsageStore` never fails a request; `MemoryUsageStore` built in, Redis/kit backends are ~5 lines). Pair with `createApp({ rateLimit: { plan: { resolve, limits: { free: { max: 60 }, enterprise: false }, default: 'free' } } })` for per-plan ceilings — `false` = unlimited, unknown/throwing resolvers fall back to `default` then global `max`, and buckets follow the tenant key chain by default. Caveat: the limiter runs before route auth, so `resolve` sees the raw request.
729
729
 
730
- ## Runtime capability registry (2.34)
730
+ ## Runtime capability registry (2.33)
731
731
 
732
732
  Under `runtime: 'distributed'`, subsystems holding process-local state must declare it — the end-of-boot audit FAILS the app on undeclared memory state, naming every violator:
733
733
 
@@ -126,7 +126,7 @@ import {
126
126
  import { tracingPlugin } from '@classytic/arc/plugins/tracing';
127
127
  import { auditPlugin } from '@classytic/arc/audit';
128
128
  import { idempotencyPlugin } from '@classytic/arc/idempotency';
129
- // Runtime capability registry (2.34) — declare process-local state so a
129
+ // Runtime capability registry (2.33) — declare process-local state so a
130
130
  // `runtime: 'distributed'` boot fails on undeclared memory state:
131
131
  import { declareRuntimeCapability } from '@classytic/arc/utils';
132
132
  ```
@@ -1,25 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- //#region src/core/actionPermissions.ts
3
- /**
4
- * Return the effective `PermissionCheck` for a single action, or `undefined`
5
- * when the resource declares no gate at any level.
6
- *
7
- * Callers decide what "no gate" means:
8
- * - HTTP: boot-time throw in `normalizeActionsToRouterConfig`.
9
- * - MCP: tool-generation throw in `resourceToTools` (mirrors HTTP — the
10
- * two surfaces fail closed identically so MCP can't expose an
11
- * unauthenticated mutating tool when the HTTP plugin lifecycle hasn't
12
- * run).
13
- * - OpenAPI: docs advertise the endpoint as unauthenticated.
14
- */
15
- function resolveActionPermission(input) {
16
- const { action, resourcePermissions, resourceActionPermissions, globalAuth } = input;
17
- const explicit = typeof action !== "function" && action.permissions ? action.permissions : void 0;
18
- if (explicit) return explicit;
19
- if (resourceActionPermissions) return resourceActionPermissions;
20
- if (globalAuth) return globalAuth;
21
- const updateFallback = resourcePermissions?.update;
22
- if (updateFallback) return updateFallback;
23
- }
24
- //#endregion
25
- export { resolveActionPermission as t };
@@ -1,48 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- //#region src/core/aggregation/defineAggregation.ts
3
- /**
4
- * Declare a single resource aggregation. Exported configs flow into
5
- * `defineResource({ aggregations: { ... } })` either as named keys or
6
- * via auto-discovery patterns (loadAggregations — future).
7
- *
8
- * @example Inline
9
- * ```ts
10
- * defineResource({
11
- * name: 'order',
12
- * aggregations: {
13
- * revenueByStatus: defineAggregation({
14
- * groupBy: 'status',
15
- * measures: { count: 'count', revenue: 'sum:totalPrice' },
16
- * permissions: requireRoles(['admin']),
17
- * }),
18
- * },
19
- * });
20
- * ```
21
- *
22
- * @example Multi-file
23
- * ```ts
24
- * // orders/aggregations/revenue-by-status.ts
25
- * import { defineAggregation } from '@classytic/arc';
26
- *
27
- * export const revenueByStatus = defineAggregation({
28
- * groupBy: 'status',
29
- * measures: { count: 'count', revenue: 'sum:totalPrice' },
30
- * permissions: requireRoles(['admin']),
31
- * timeout: 5000,
32
- * cache: { staleTime: 60 },
33
- * });
34
- *
35
- * // orders/order.resource.ts
36
- * import * as aggregations from './aggregations/index.js';
37
- *
38
- * defineResource({
39
- * name: 'order',
40
- * aggregations, // 30+ entries flow in
41
- * });
42
- * ```
43
- */
44
- function defineAggregation(config) {
45
- return config;
46
- }
47
- //#endregion
48
- export { defineAggregation as t };
@@ -1,46 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- //#region src/factory/preloadResources.ts
3
- /**
4
- * Normalize an eager `import.meta.glob` result into a `ResourceLike[]`.
5
- *
6
- * Accepts either:
7
- * - `{ import: 'default' }` form: values are the resource directly
8
- * - default form: values are the full module — picks first export with `toPlugin()`
9
- *
10
- * Throws if any module doesn't yield a valid `ResourceLike`.
11
- */
12
- function preloadResources(globResult) {
13
- const resources = [];
14
- for (const [path, value] of Object.entries(globResult)) {
15
- const resource = pickResource(value);
16
- if (!resource) throw new Error(`preloadResources: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
17
- resources.push(resource);
18
- }
19
- return resources.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
20
- }
21
- /**
22
- * Normalize a lazy `import.meta.glob` result into a `Promise<ResourceLike[]>`.
23
- *
24
- * Use this when resources depend on prior bootstrap (e.g., engine init) and
25
- * cannot be evaluated at import time of the preload file.
26
- */
27
- async function preloadResourcesAsync(globResult) {
28
- return (await Promise.all(Object.entries(globResult).map(async ([path, loader]) => {
29
- const resource = pickResource(await loader());
30
- if (!resource) throw new Error(`preloadResourcesAsync: ${path} does not export a valid resource.\n Expected: a default export OR a named export with toPlugin().`);
31
- return resource;
32
- }))).sort((a, b) => (a.name ?? "").localeCompare(b.name ?? ""));
33
- }
34
- function pickResource(value) {
35
- if (!value || typeof value !== "object") return void 0;
36
- if (typeof value.toPlugin === "function") return value;
37
- const mod = value;
38
- const candidates = [
39
- mod.default,
40
- mod.resource,
41
- ...Object.values(mod)
42
- ];
43
- for (const c of candidates) if (c && typeof c === "object" && typeof c.toPlugin === "function") return c;
44
- }
45
- //#endregion
46
- export { preloadResourcesAsync as n, preloadResources as t };
@@ -1,113 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- import { n as evaluateAndApplyPermission } from "./authorizationDecision-B3mP_QTy.mjs";
3
- import { f as createError } from "./errors-Cd_yy7aW.mjs";
4
- import { u as getRequestScope } from "./types-CoqIRrkY.mjs";
5
- //#region src/core/middlewares/permissions.ts
6
- /**
7
- * Build a PermissionContext from a Fastify request. Extracted so the CRUD
8
- * permission middleware and the dynamic action-permission check use the same
9
- * field layout — divergence here silently broke policy filters for actions.
10
- */
11
- function buildPermissionContext(req, opts) {
12
- const reqWithExtras = req;
13
- const params = req.params;
14
- return {
15
- user: reqWithExtras.user ?? null,
16
- scope: getRequestScope(req),
17
- request: req,
18
- resource: opts.resource,
19
- action: opts.action,
20
- resourceId: opts.resourceId ?? params?.id,
21
- params,
22
- data: opts.data ?? req.body
23
- };
24
- }
25
- /**
26
- * Static per-route CRUD permission gate. The permission and action are known
27
- * at route-registration time, so the gate is a plain preHandler.
28
- *
29
- * Actions use the dynamic counterpart `buildActionPermissionMw` — their
30
- * permission is resolved from `body.action` at request time.
31
- */
32
- function buildCrudPermissionMw(permissionCheck, resourceName, action) {
33
- if (!permissionCheck) return null;
34
- return async (req, reply) => {
35
- const context = buildPermissionContext(req, {
36
- resource: resourceName,
37
- action
38
- });
39
- await evaluateAndApplyPermission(permissionCheck, context, req, reply);
40
- };
41
- }
42
- /**
43
- * Dynamic per-action permission gate for the action router.
44
- *
45
- * Resolves the permission from `body.action` at request time and runs
46
- * `evaluateAndApplyPermission` from the canonical `permissionMw` slot — so
47
- * `_policyFilters` and `request.scope` are installed BEFORE `pluginMw`
48
- * (idempotency) and `routeGuards` run. Previously this check lived inside
49
- * the main action handler, which meant idempotency recorded unauthorized
50
- * requests and route guards saw unfiltered scope — the very divergence
51
- * routerShared exists to prevent.
52
- *
53
- * Also acts as a defensive fallback for invalid action names — the
54
- * `oneOf` body schema normally rejects these at AJV validation, but
55
- * hosts that disable schema validation still get a 400 here.
56
- */
57
- function buildActionPermissionMw(actionEnum, actionPermissions, globalAuth, resourceName) {
58
- const enumSet = new Set(actionEnum);
59
- const validActions = [...actionEnum];
60
- return async (req, reply) => {
61
- const body = req.body ?? {};
62
- const action = body.action;
63
- if (!action || !enumSet.has(action)) throw createError(400, `Invalid action '${action ?? ""}'. Valid actions: ${validActions.join(", ")}`, { validActions });
64
- const permissionCheck = actionPermissions[action] ?? globalAuth;
65
- if (!permissionCheck) return;
66
- const { action: _discard, ...data } = body;
67
- const params = req.params;
68
- const context = buildPermissionContext(req, {
69
- resource: resourceName,
70
- action,
71
- resourceId: params?.id,
72
- data
73
- });
74
- await evaluateAndApplyPermission(permissionCheck, context, req, reply, { defaultDenialMessage: (user) => user ? `Permission denied for '${action}'` : "Authentication required" });
75
- };
76
- }
77
- //#endregion
78
- //#region src/core/middlewares/registerRoute.ts
79
- /**
80
- * Register a route with friendly handling of `FST_ERR_DUPLICATED_ROUTE`.
81
- *
82
- * Boot-time `validateRouteCrudCollisions()` catches custom-route → auto-CRUD
83
- * overlaps before Fastify ever runs, but a few cases still slip through to
84
- * Fastify itself: two presets contributing the same `routes` entry, hosts
85
- * mounting two resources at the same prefix, custom routes that share a
86
- * URL with another plugin. Fastify's default message ("Method 'GET'
87
- * already declared for route '/'") gives no hint at the resource-shaped
88
- * fix — `disabledRoutes: ['list']` — so wrap it with one.
89
- */
90
- function tryRegisterRoute(fastify, opts, context) {
91
- try {
92
- fastify.route(opts);
93
- } catch (err) {
94
- const fastifyErr = err;
95
- if (fastifyErr?.code !== "FST_ERR_DUPLICATED_ROUTE") throw err;
96
- const method = opts.method;
97
- const url = opts.url;
98
- const opHint = context.op ? ` (op: ${context.op})` : "";
99
- const fixHint = context.op && [
100
- "list",
101
- "get",
102
- "create",
103
- "update",
104
- "delete"
105
- ].includes(context.op) ? ` Suppress the auto-CRUD route with \`disabledRoutes: ['${context.op}']\`.` : " If a custom route in `routes:` collides with auto-CRUD, add `disabledRoutes: ['list' | 'get' | 'create' | 'update' | 'delete']` to `defineResource()` to suppress the matching auto-CRUD route.";
106
- const enhanced = /* @__PURE__ */ new Error(`[arc] Duplicate route ${method} ${url} on resource "${context.resourceName}"${opHint}.${fixHint} Original: ${fastifyErr.message ?? "FST_ERR_DUPLICATED_ROUTE"}`);
107
- enhanced.code = "FST_ERR_DUPLICATED_ROUTE";
108
- enhanced.cause = err;
109
- throw enhanced;
110
- }
111
- }
112
- //#endregion
113
- export { buildActionPermissionMw as n, buildCrudPermissionMw as r, tryRegisterRoute as t };
@@ -1,45 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- import { arcLog } from "./logger/index.mjs";
3
- import { policyRecordToFilter } from "@classytic/repo-core/filter";
4
- //#region src/core/repositoryFilter.ts
5
- const log = arcLog("repository-filter");
6
- let warnedUnconvertible = false;
7
- /** True for a `{ $op: ... }` operator object (all keys `$`-prefixed). */
8
- function isOperatorObject(value) {
9
- if (value === null || typeof value !== "object") return false;
10
- if (Array.isArray(value) || value instanceof Date) return false;
11
- const keys = Object.keys(value);
12
- return keys.length > 0 && keys.some((k) => k.startsWith("$"));
13
- }
14
- /**
15
- * Does this compound filter contain any `$`-operator (top-level logical like
16
- * `$and`/`$or`, or a field operator object like `{ $in: [...] }`)? If not, it is
17
- * pure flat equality and portable as-is.
18
- */
19
- function filterHasDollarOperator(record) {
20
- for (const [key, value] of Object.entries(record)) {
21
- if (key.startsWith("$")) return true;
22
- if (isOperatorObject(value)) return true;
23
- }
24
- return false;
25
- }
26
- /**
27
- * Normalize a compound query/policy filter for the repository layer. Flat
28
- * equality records pass through unchanged; anything carrying `$`-operators is
29
- * converted to the portable repo-core {@link Filter} IR so it compiles
30
- * identically on Mongo, SQLite, Postgres, and custom adapters.
31
- */
32
- function toRepositoryFilter(record) {
33
- if (!filterHasDollarOperator(record)) return record;
34
- try {
35
- return policyRecordToFilter(record);
36
- } catch (err) {
37
- if (!warnedUnconvertible) {
38
- warnedUnconvertible = true;
39
- log.warn(`A \`\$\`-operator filter could not be normalized to portable Filter IR (operator outside arc's policy dialect). Passing the raw record to the repository — MongoKit handles it; non-Mongo kits may not. (${err instanceof Error ? err.message : String(err)})`);
40
- }
41
- return record;
42
- }
43
- }
44
- //#endregion
45
- export { toRepositoryFilter as t };
@@ -1,69 +0,0 @@
1
- /*! @classytic/arc v2.34.0 | MIT | https://classytic.com */
2
- //#region src/utils/simpleEqualityMatcher.ts
3
- /**
4
- * `simpleEqualityMatcher` — a minimal, dialect-agnostic flat-key equality
5
- * matcher for `DataAdapter.matchesFilter` / `BaseController({ matchesFilter })`.
6
- *
7
- * **What it does:** for each `[key, expected]` in the filter, compares
8
- * `item[key]` to `expected` via string coercion (so Mongo `ObjectId` values
9
- * match their string representation) and returns `true` only if every
10
- * filter entry matches. Array item values are matched implicitly (contains).
11
- *
12
- * **What it does NOT do:**
13
- * - No `$eq` / `$ne` / `$in` / `$nin` / `$gt` / `$lt` / `$regex` / `$exists`
14
- * - No `$and` / `$or`
15
- * - No dot-path traversal (`"owner.id"`)
16
- * - No schema-specific coercion
17
- *
18
- * **Why it exists:** 95%+ of arc's `_policyFilters` are produced by built-in
19
- * permission helpers and are shaped like `{ ownerId: "u1" }` or
20
- * `{ organizationId: "org_x" }` — flat equality. For that common shape,
21
- * this helper is a safe, tested, 15-line defense-in-depth matcher that
22
- * hosts using minimal repos (no `getOne(compoundFilter)` DB path) can opt
23
- * into without arc shipping a full Mongo-syntax engine.
24
- *
25
- * **When to use:**
26
- * - Your adapter/repo doesn't natively filter on `getOne(compoundFilter)`
27
- * - Your `_policyFilters` are flat equality (from arc's built-in permission helpers)
28
- * - You want defense-in-depth on `validateItemAccess` / `fetchDetailed`'s `getById` fallback
29
- *
30
- * **When NOT to use:**
31
- * - Your `_policyFilters` use operators (`$in`, `$ne`, etc.) — supply a
32
- * native matcher (mongokit's repo does the filter at the DB layer; for
33
- * custom repos, wrap the kit's own predicate engine).
34
- * - You're a mongokit / sqlitekit / Prisma user — the DB-level filter
35
- * applied by `getOne(compoundFilter)` already covers this.
36
- *
37
- * @example
38
- * ```ts
39
- * import { simpleEqualityMatcher } from '@classytic/arc/utils';
40
- *
41
- * // On a custom adapter
42
- * const adapter: DataAdapter = {
43
- * repository,
44
- * type: 'custom',
45
- * name: 'in-memory',
46
- * matchesFilter: simpleEqualityMatcher,
47
- * };
48
- *
49
- * // Or directly on BaseController for ad-hoc controllers
50
- * new BaseController(repo, { matchesFilter: simpleEqualityMatcher });
51
- * ```
52
- */
53
- function simpleEqualityMatcher(item, filters) {
54
- if (!item || typeof item !== "object") return false;
55
- const obj = item;
56
- for (const [key, expected] of Object.entries(filters)) {
57
- if (expected && typeof expected === "object" && !Array.isArray(expected) && Object.getPrototypeOf(expected) === Object.prototype && Object.keys(expected).some((k) => k.startsWith("$"))) return false;
58
- const actual = obj[key];
59
- if (Array.isArray(actual)) {
60
- const expectedStr = String(expected);
61
- if (!actual.some((v) => String(v) === expectedStr)) return false;
62
- continue;
63
- }
64
- if (String(actual) !== String(expected)) return false;
65
- }
66
- return true;
67
- }
68
- //#endregion
69
- export { simpleEqualityMatcher as t };