@classytic/arc 2.23.0 → 2.28.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 (147) hide show
  1. package/README.md +2 -0
  2. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController-B65zbz9m.mjs} +94 -23
  3. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/{applyPermissionResult-Tw4iXtlf.mjs → applyPermissionResult-BnyVE9eD.mjs} +10 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +263 -259
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cleanup/index.d.mts +313 -0
  14. package/dist/cleanup/index.mjs +1267 -0
  15. package/dist/cli/commands/describe.d.mts +1 -1
  16. package/dist/cli/commands/docs.mjs +1 -1
  17. package/dist/cli/commands/generate.mjs +1 -1
  18. package/dist/cli/commands/init.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +5 -4
  23. package/dist/core-BGa8TXJR.mjs +221 -0
  24. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-C-N7z2EL.mjs} +3 -2
  25. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/{createApp-CjOeWJ9_.mjs → createApp-Du5G54TM.mjs} +187 -96
  27. package/dist/{core-DQ6rDTyH.mjs → defineResource-2zvVVpxg.mjs} +297 -230
  28. package/dist/docs/index.d.mts +1 -1
  29. package/dist/docs/index.mjs +2 -2
  30. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  31. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  32. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  33. package/dist/events/index.d.mts +83 -314
  34. package/dist/events/index.mjs +183 -95
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  37. package/dist/factory/index.d.mts +5 -5
  38. package/dist/factory/index.mjs +30 -8
  39. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-OzYGIiU1.d.mts} +26 -2
  40. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  41. package/dist/headers-BvlQSDMB.mjs +45 -0
  42. package/dist/health-CB_m4S32.mjs +377 -0
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/idempotency/index.mjs +27 -3
  45. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/index-CZY4EGga.d.mts +107 -0
  48. package/dist/{index-CC5ruDGh.d.mts → index-CjVg-Epg.d.mts} +37 -5
  49. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +7 -6
  52. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +4 -4
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +37 -30
  63. package/dist/integrations/webhooks.mjs +51 -62
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cs6nRGCt.mjs} +12 -10
  69. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  70. package/dist/middleware/index.d.mts +17 -1
  71. package/dist/middleware/index.mjs +1 -1
  72. package/dist/migrations/index.d.mts +94 -44
  73. package/dist/migrations/index.mjs +196 -44
  74. package/dist/module-_-M7C5ES.mjs +29 -0
  75. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  76. package/dist/{openapi-jSDx-VDv.mjs → openapi-BLV4OMXb.mjs} +3 -3
  77. package/dist/permissions/index.d.mts +2 -2
  78. package/dist/permissions/index.mjs +3 -3
  79. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  80. package/dist/pipeline/index.d.mts +1 -1
  81. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BGRnV-To.mjs} +4 -27
  82. package/dist/plugins/index.d.mts +6 -77
  83. package/dist/plugins/index.mjs +11 -95
  84. package/dist/plugins/response-cache.mjs +1 -0
  85. package/dist/plugins/tracing-entry.mjs +1 -1
  86. package/dist/{index-Ba10fdKp.d.mts → preloadResources-CM4uUaFv.d.mts} +51 -50
  87. package/dist/preloadResources-DSY5HGpS.mjs +45 -0
  88. package/dist/presets/filesUpload.d.mts +3 -3
  89. package/dist/presets/filesUpload.mjs +3 -3
  90. package/dist/presets/index.d.mts +2 -1
  91. package/dist/presets/index.mjs +1 -1
  92. package/dist/presets/multiTenant.d.mts +1 -1
  93. package/dist/presets/multiTenant.mjs +1 -1
  94. package/dist/presets/search.d.mts +2 -2
  95. package/dist/presets/search.mjs +1 -1
  96. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  97. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  98. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  99. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  100. package/dist/{realtime-I8OrfnMR.mjs → realtime-DEuDu9io.mjs} +3 -3
  101. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  102. package/dist/registerRoute-DTp18Ah_.mjs +108 -0
  103. package/dist/registry/index.d.mts +2 -2
  104. package/dist/registry/index.mjs +1 -1
  105. package/dist/{registry-vcOCtPij.mjs → registry-9km_w1aP.mjs} +15 -0
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/schemas/index.d.mts +67 -67
  110. package/dist/schemas/index.mjs +2 -3
  111. package/dist/scim/index.mjs +1 -2
  112. package/dist/scope/index.d.mts +2 -2
  113. package/dist/scope/index.mjs +4 -4
  114. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  115. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  116. package/dist/testing/cleanupStoreContract.d.mts +10 -0
  117. package/dist/testing/cleanupStoreContract.mjs +375 -0
  118. package/dist/testing/index.d.mts +5 -3
  119. package/dist/testing/index.mjs +4 -2
  120. package/dist/testing/outboxStoreContract.d.mts +13 -0
  121. package/dist/testing/outboxStoreContract.mjs +247 -0
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/types-C_ZZRcD6.d.mts +450 -0
  126. package/dist/{module-B4u10iOJ.d.mts → types-Cu6vVJmh.d.mts} +196 -60
  127. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  128. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  129. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  130. package/dist/usage/index.d.mts +3 -52
  131. package/dist/usage/index.mjs +0 -0
  132. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  133. package/dist/utils/index.d.mts +58 -85
  134. package/dist/utils/index.mjs +4 -2
  135. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  136. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  137. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  138. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  139. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  140. package/package.json +541 -513
  141. package/skills/arc/references/api-reference.md +3 -1
  142. package/skills/arc/references/integrations.md +20 -11
  143. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  144. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  145. package/dist/health-ScyjIVo5.mjs +0 -202
  146. package/dist/requestId-5BhTVzBv.mjs +0 -113
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,5 +1,106 @@
1
+ import { r as EventHandler } from "./EventTransport-QmY7A258.mjs";
1
2
  import { n as ErrorMapper } from "./errorHandler-C6yv4ZTW.mjs";
2
- import { FastifyInstance } from "fastify";
3
+ import { FastifyInstance, FastifyPluginAsync } from "fastify";
4
+ import { LockAdapter } from "@classytic/repo-core/lock";
5
+ //#region src/plugins/health.d.ts
6
+ interface HealthCheck {
7
+ /** Name of the dependency */
8
+ name: string;
9
+ /**
10
+ * Function that returns true if healthy, false otherwise.
11
+ *
12
+ * Receives an `AbortSignal` aborted when the check's timeout elapses —
13
+ * pass it to fetch/driver calls so a stalled dependency probe actually
14
+ * STOPS instead of accumulating an abandoned operation per readiness
15
+ * poll. Ignoring the parameter is fine (back-compat): the timeout still
16
+ * bounds how long the PROBE waits, just not the underlying work.
17
+ */
18
+ check: (signal?: AbortSignal) => Promise<boolean> | boolean;
19
+ /** Optional timeout in ms (default: 5000) */
20
+ timeout?: number;
21
+ /** Whether this check is critical for readiness (default: true) */
22
+ critical?: boolean;
23
+ }
24
+ interface HealthOptions {
25
+ /** Route prefix (default: '/_health') */
26
+ prefix?: string;
27
+ /** Health check dependencies */
28
+ checks?: readonly HealthCheck[];
29
+ /** Enable metrics endpoint (default: false) */
30
+ metrics?: boolean;
31
+ /** Custom metrics collector function */
32
+ metricsCollector?: () => Promise<string> | string;
33
+ /** Version info to include in responses */
34
+ version?: string;
35
+ /** Collect HTTP request metrics (default: true if metrics enabled) */
36
+ collectHttpMetrics?: boolean;
37
+ }
38
+ declare const healthPlugin: FastifyPluginAsync<HealthOptions>;
39
+ declare const _default$1: FastifyPluginAsync<HealthOptions>;
40
+ //#endregion
41
+ //#region src/plugins/schedules.d.ts
42
+ interface ScheduleDefinition {
43
+ /** Unique name — also the lock key (`arc:schedule:<name>`). */
44
+ name: string;
45
+ /** Interval in milliseconds between the END of one run and the next tick. */
46
+ every: number;
47
+ /** The work. Receives the Fastify instance; errors are logged, never fatal. */
48
+ handler: (fastify: FastifyInstance) => void | Promise<void>;
49
+ /** Fire once immediately when the app becomes ready (default: false). */
50
+ runOnStart?: boolean;
51
+ /**
52
+ * Random extra delay in `[0, jitterMs)` added to the FIRST tick only.
53
+ * Spreads boot-aligned jobs so multiple same-cadence schedules don't all
54
+ * fire at exactly `boot + every` after every restart (and, with a lock,
55
+ * de-synchronizes replicas competing for the same lease). Ignored when
56
+ * `runOnStart` is true.
57
+ */
58
+ jitterMs?: number;
59
+ /**
60
+ * Lease duration when a `lock` is configured. Default: `every * 0.9`
61
+ * (capped at 5 minutes) — long enough that other replicas skip the same
62
+ * tick, short enough that a crashed leader fails over within one cycle.
63
+ */
64
+ leaseMs?: number;
65
+ /** Per-schedule kill switch (default: true). */
66
+ enabled?: boolean;
67
+ }
68
+ interface SchedulesPluginOptions {
69
+ schedules: readonly ScheduleDefinition[];
70
+ /**
71
+ * Ecosystem lock adapter (`LockAdapter` from `@classytic/repo-core/lock`)
72
+ * for multi-replica deployments. Omit for single-instance apps — every
73
+ * tick runs locally.
74
+ */
75
+ lock?: LockAdapter;
76
+ /**
77
+ * Identity of this replica for lease ownership. Default: a random id per
78
+ * process (repo-core's `getInstanceId()` is a good explicit value).
79
+ */
80
+ holderId?: string;
81
+ /**
82
+ * Master switch — `false` registers a typed no-op (config-gated
83
+ * environments), same pattern as `idempotencyPlugin`.
84
+ */
85
+ enabled?: boolean;
86
+ }
87
+ /** Per-schedule runtime counters, readable via `fastify.getScheduleStats()`. */
88
+ interface ScheduleStats {
89
+ name: string;
90
+ runs: number;
91
+ failures: number;
92
+ skippedByLock: number;
93
+ lastRunAt: string | null;
94
+ lastError: string | null;
95
+ }
96
+ declare module "fastify" {
97
+ interface FastifyInstance {
98
+ getScheduleStats?: () => ScheduleStats[];
99
+ }
100
+ }
101
+ declare const schedulesPlugin: FastifyPluginAsync<SchedulesPluginOptions>;
102
+ declare const _default: FastifyPluginAsync<SchedulesPluginOptions>;
103
+ //#endregion
3
104
  //#region src/factory/loadResources.d.ts
4
105
  /**
5
106
  * loadResources — Auto-discover resource files from a directory.
@@ -218,7 +319,31 @@ interface LoadResourcesOptions<TContext = unknown> {
218
319
  */
219
320
  declare function loadResources<TContext = unknown>(dir: string, options?: LoadResourcesOptions<TContext>): Promise<ResourceLike[]>;
220
321
  //#endregion
221
- //#region src/factory/module.d.ts
322
+ //#region src/factory/module/types.d.ts
323
+ /**
324
+ * A module's contribution of some arm (event handlers, scheduled jobs, …).
325
+ * Either a static array or a factory resolved AFTER all module bootstraps, so a
326
+ * contribution can close over booted engines / the Fastify instance instead of
327
+ * reaching for a global getter (the coupling the module contract removes).
328
+ */
329
+ type ModuleContribution<T> = readonly T[] | ((fastify: FastifyInstance) => readonly T[] | Promise<readonly T[]>);
330
+ /**
331
+ * A domain-event subscription a module owns. Subscribed in dependency order
332
+ * during the `afterResources` phase (routes mounted, engines booted) and
333
+ * auto-unsubscribed at shutdown BEFORE module `onClose` (deps still alive).
334
+ */
335
+ interface EventHandlerDefinition {
336
+ /** Event pattern(s) to subscribe — same matcher as `fastify.events.subscribe`. */
337
+ event: string | readonly string[];
338
+ /** The subscriber. */
339
+ handler: EventHandler;
340
+ /**
341
+ * Optional stable name — enables duplicate detection + teardown diagnostics.
342
+ * Third-party modules should prefix it with their module name, for example
343
+ * `inventory.order-created`; Arc does not silently rewrite public names.
344
+ */
345
+ name?: string;
346
+ }
222
347
  interface ArcModule<TExports = unknown> {
223
348
  /** Stable identifier — appears in boot logs and duplicate-module detection. */
224
349
  readonly name: string;
@@ -344,6 +469,74 @@ interface ArcModule<TExports = unknown> {
344
469
  * module owns.
345
470
  */
346
471
  afterResources?: (fastify: FastifyInstance) => void | Promise<void>;
472
+ /**
473
+ * Readiness checks this module contributes to the app's
474
+ * `healthPlugin` (`/_health/ready`). Collected across ALL modules in
475
+ * dependency order and merged BEFORE the host's app-level `arcPlugins.health`
476
+ * checks (modules-first, host-last) — the same additive convention arc uses
477
+ * for resources and error mappers. Check `name`s must be unique across the
478
+ * module graph (arc fails at boot on a collision, attributing both owners).
479
+ *
480
+ * STATIC by design (not a factory): declare the check up front and let its
481
+ * `check()` closure resolve dependencies LAZILY at probe time, so collection
482
+ * can happen before the health plugin registers and the worker probe
483
+ * (`createWorker`) receives the identical union without a second registration.
484
+ *
485
+ * ```ts
486
+ * defineModule({ name: "inventory", healthChecks: [
487
+ * { name: "flow-engine", check: () => flow.isReady() },
488
+ * ] })
489
+ * ```
490
+ */
491
+ readonly healthChecks?: readonly HealthCheck[];
492
+ /**
493
+ * Domain-event subscriptions this module owns — the declarative replacement
494
+ * for imperative `fastify.events.subscribe(...)` calls inside
495
+ * `afterResources`. Subscribed in dependency order during the
496
+ * `afterResources` phase (routes mounted, engines booted), and arc RETAINS
497
+ * every unsubscribe and invokes it at shutdown BEFORE module `onClose` (while
498
+ * module deps are still alive). Named handlers must be unique across the
499
+ * module graph; a module that declares handlers while the event subsystem is
500
+ * disabled (`arcPlugins.events: false`) fails at boot. Array or factory (the
501
+ * factory runs after bootstraps, so it can close over booted engines).
502
+ *
503
+ * ```ts
504
+ * defineModule({ name: "party", eventHandlers: [
505
+ * { name: "party.link-customer", event: ["customer:created"], handler: onCustomer },
506
+ * ] })
507
+ * ```
508
+ */
509
+ readonly eventHandlers?: ModuleContribution<EventHandlerDefinition>;
510
+ /**
511
+ * Durable workflows this module owns. OPAQUE to arc core — arc never imports
512
+ * `@classytic/streamline`; the value flows through untouched and the
513
+ * streamline integration (`@classytic/arc/integrations/streamline`, a peer)
514
+ * gives it meaning (name/shape validation, registration). Collected via
515
+ * `collectModuleWorkflows` at integration-init time (after module bootstraps),
516
+ * so a factory can close over the shared workflow container the integration
517
+ * decorates on the instance.
518
+ *
519
+ * ```ts
520
+ * defineModule({ name: "invoice",
521
+ * workflows: (f) => createInvoiceWorkflows(f.streamlineContainer) })
522
+ * ```
523
+ */
524
+ readonly workflows?: ModuleContribution<unknown>;
525
+ /**
526
+ * Recurring interval schedules this module owns. Arc composes them into its
527
+ * existing `schedulesPlugin` after bootstraps, in dependency order. Names
528
+ * must be unique across the module graph. Configure multi-replica locking via
529
+ * `arcPlugins.schedules`; explicitly disabling that runner while a module
530
+ * declares schedules fails boot instead of silently dropping work. Array or
531
+ * factory (the factory runs once after bootstraps and may close over engines).
532
+ *
533
+ * ```ts
534
+ * defineModule({ name: "loyalty", scheduledJobs: [
535
+ * { name: "loyalty.point.expiration", every: 3_600_000, handler: () => sweep() },
536
+ * ] })
537
+ * ```
538
+ */
539
+ readonly scheduledJobs?: ModuleContribution<ScheduleDefinition>;
347
540
  /**
348
541
  * Teardown — registered as a Fastify `onClose` hook. Destroy engines, stop
349
542
  * timers, flush the module's outbox. Modules close in REVERSE list order
@@ -375,31 +568,6 @@ interface ArcModule<TExports = unknown> {
375
568
  * ```
376
569
  */
377
570
  type ArcModuleInput<TExports = unknown> = ArcModule<TExports> | Promise<ArcModule<TExports>> | (() => ArcModule<TExports> | Promise<ArcModule<TExports>>);
378
- /**
379
- * Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
380
- * (this is where a dynamic `import()` fires); a promise (or plain module) is
381
- * awaited. Called once per module at the start of the bootstrap phase.
382
- */
383
- declare function resolveModule(input: ArcModuleInput): Promise<ArcModule>;
384
- /**
385
- * Order modules for composition by their `dependsOn` edges — a STABLE
386
- * topological sort. Called once at the start of the bootstrap phase; every
387
- * subsequent phase (bootstrap, resources, afterResources, and reverse-order
388
- * onClose) iterates the returned list, so a module's declared dependencies are
389
- * always composed before it.
390
- *
391
- * "Stable" = modules with no edge between them keep their original list order,
392
- * so declaring `dependsOn` on one module never silently reorders an unrelated
393
- * one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
394
- * returned unchanged.
395
- *
396
- * Fail-fast — throws (never reorders past a broken contract) on:
397
- * - duplicate module names (the name is the graph key)
398
- * - a `dependsOn` name not present in the composed set
399
- * - a self-reference (`dependsOn` includes the module's own name)
400
- * - a dependency cycle (reports the concrete `a → b → … → a` path)
401
- */
402
- declare function orderModules(modules: readonly ArcModule[]): ArcModule[];
403
571
  /**
404
572
  * Typed module registry — hosts augment this via declaration merging so
405
573
  * `getModuleExports(f, "order")` infers the export type without a manual type
@@ -418,37 +586,5 @@ declare function orderModules(modules: readonly ArcModule[]): ArcModule[];
418
586
  * available for apps that don't augment it.
419
587
  */
420
588
  interface ArcModuleRegistry {}
421
- /**
422
- * Identity helper for authoring a typed module — mirrors `defineResource`.
423
- * Gives inference + a single obvious construction site; does no work.
424
- *
425
- * `TExports` is inferred from the `bootstrap` return value, so a module
426
- * author gets a typed public export for free:
427
- *
428
- * ```ts
429
- * export const accountingModule = (deps: Deps) =>
430
- * defineModule({
431
- * name: "accounting",
432
- * bootstrap: async () => createAccountingEngine(deps), // TExports inferred
433
- * });
434
- * ```
435
- */
436
- declare function defineModule<TExports = unknown>(module: ArcModule<TExports>): ArcModule<TExports>;
437
- /**
438
- * Typed accessor for a module's public export (its `bootstrap` return value),
439
- * recorded at `fastify.arc.modules[name]`.
440
- *
441
- * Throwing, in line with the fail-fast contract (and `requireOrgId`-style
442
- * accessors): a missing entry means the module either isn't composed, is
443
- * composed AFTER the caller (list order = init order), or returned nothing —
444
- * all wiring bugs that must surface at boot, not as `undefined` downstream.
445
- *
446
- * The type parameter is an assertion, not a proof. Two ways to type it:
447
- * - augment `ArcModuleRegistry` once — then the name alone infers the type:
448
- * `getModuleExports(f, "accounting")` → `AccountingEngine`.
449
- * - or pass it inline: `getModuleExports<AccountingEngine>(f, "accounting")`.
450
- */
451
- declare function getModuleExports<K extends keyof ArcModuleRegistry>(fastify: FastifyInstance, name: K): ArcModuleRegistry[K];
452
- declare function getModuleExports<TExports = unknown>(fastify: FastifyInstance, name: string): TExports;
453
589
  //#endregion
454
- export { getModuleExports as a, LoadResourcesOptions as c, loadResources as d, defineModule as i, ResourceLike as l, ArcModuleInput as n, orderModules as o, ArcModuleRegistry as r, resolveModule as s, ArcModule as t, ResourceModule as u };
590
+ export { _default$1 as _, ModuleContribution as a, ResourceModule as c, ScheduleStats as d, SchedulesPluginOptions as f, HealthOptions as g, HealthCheck as h, EventHandlerDefinition as i, loadResources as l, schedulesPlugin as m, ArcModuleInput as n, LoadResourcesOptions as o, _default as p, ArcModuleRegistry as r, ResourceLike as s, ArcModule as t, ScheduleDefinition as u, healthPlugin as v };
@@ -1,24 +1,39 @@
1
- //#region src/scope/types.d.ts
1
+ //#region src/utils/headers.d.ts
2
2
  /**
3
- * Request Scope — The One Standard
4
- *
5
- * Discriminated union representing the access context of every request.
6
- * Replaces scattered orgScope/orgRoles/organizationId/bypassRoles.
7
- *
8
- * Set once by auth adapters, read everywhere by permissions/presets/guards.
3
+ * Canonical header typing + accessor.
4
+ *
5
+ * Node/Fastify header values are `string | string[] | undefined` — repeated
6
+ * headers arrive as arrays. `IRequestContext.headers` is DECLARED as
7
+ * `Record<string, string | undefined>` for ergonomic host code, but the HTTP
8
+ * adapter passes Fastify's headers object through unchanged, so `string[]`
9
+ * CAN appear at runtime behind the narrower type (the declared type is
10
+ * scheduled to widen to {@link ArcHeaders} in v3). Until then, read headers
11
+ * through {@link getHeader} instead of indexing — it's honest about arrays
12
+ * and deterministic about duplicates.
13
+ */
14
+ /** What a headers bag actually contains at runtime — HTTP or synthetic MCP. */
15
+ type ArcHeaders = Readonly<Record<string, string | readonly string[] | undefined>>;
16
+ /**
17
+ * Read a single header value. Lowercases the name (Node normalizes incoming
18
+ * header names to lowercase) and returns the FIRST value when the header
19
+ * was repeated.
20
+ *
21
+ * Duplicate policy: first-value is deterministic, but for security-sensitive
22
+ * headers (authorization material, forwarded-for chains, content-length) a
23
+ * duplicate usually signals smuggling or a broken proxy — consumers that
24
+ * care should check `Array.isArray(headers[name])` and REJECT instead of
25
+ * picking a winner.
26
+ */
27
+ declare function getHeader(headers: ArcHeaders | Record<string, string | undefined> | undefined, name: string): string | undefined;
28
+ /**
29
+ * Read an identity-bearing or protocol header and reject repeated values.
9
30
  *
10
- * @example
11
- * ```typescript
12
- * // In a permission check
13
- * const scope = request.scope;
14
- * if (isElevated(scope)) return true;
15
- * if (isMember(scope) && scope.orgRoles.includes('admin')) return true;
16
- *
17
- * // Get user identity from scope
18
- * const userId = getUserId(scope);
19
- * const globalRoles = getUserRoles(scope);
20
- * ```
31
+ * Repeated security-sensitive headers are ambiguous at proxy boundaries and
32
+ * may indicate request smuggling. Callers must not silently choose a winner.
21
33
  */
34
+ declare function requireSingleHeaderValue(headers: ArcHeaders | Record<string, string | undefined> | undefined, name: string): string | undefined;
35
+ //#endregion
36
+ //#region src/scope/types.d.ts
22
37
  /**
23
38
  * A capability mandate — declarative, time-boxed, optionally cap-bounded
24
39
  * authorization for a single high-value action by an AI agent or service.
@@ -118,7 +133,9 @@ interface Mandate {
118
133
  * | elevated | Platform admin, explicit elevation |
119
134
  *
120
135
  * **Identity fields by kind:**
121
- * - `userId` / `userRoles` — available on `authenticated`, `member`, `elevated` (a real human)
136
+ * - `userId` — available on `authenticated`, `member`, `elevated` (a real human)
137
+ * - `userRoles` — available on `authenticated` and `member`; elevated scope
138
+ * bypasses role gates explicitly and therefore does not carry role claims
122
139
  * - `clientId` — available on `service` (a machine identity, NOT a user)
123
140
  * - `organizationId` — required on `member` and `service`, optional on `elevated`
124
141
  * - `orgRoles` — org-level roles, only on `member` (from membership records)
@@ -517,9 +534,90 @@ declare function getOrgContext(request: {
517
534
  declare function getRequestScope(request: {
518
535
  scope?: RequestScope;
519
536
  }): RequestScope;
537
+ /** Options for {@link scopeFirstCtx}. */
538
+ interface ScopeFirstCtxOptions {
539
+ /**
540
+ * Tenant-selection header consulted ONLY when the scope carries no org —
541
+ * the custom-auth/public bridge (same semantics as `getTenantFromRequest`).
542
+ * A validated scope org always wins; the header never overrides it.
543
+ * Default `'x-organization-id'`; pass `false` to disable the bridge and
544
+ * read strictly from scope.
545
+ */
546
+ orgHeader?: string | false;
547
+ /**
548
+ * Actor id substituted when the scope carries no user/client identity
549
+ * (e.g. `'system'` / `'anonymous'`). When provided, the returned `actorId`
550
+ * narrows to `string`; omit to receive `actorId: undefined` on
551
+ * unauthenticated requests and apply your own policy.
552
+ */
553
+ fallbackActorId?: string;
554
+ }
555
+ /** Return shape of {@link scopeFirstCtx}. */
556
+ interface ScopeFirstCtx {
557
+ actorId?: string;
558
+ organizationId?: string;
559
+ }
560
+ /**
561
+ * Request shape accepted by {@link scopeFirstCtx} — deliberately structural so
562
+ * ONE helper serves both surfaces:
563
+ * - a raw `FastifyRequest`, whose `scope` is the discriminated {@link RequestScope}
564
+ * - arc's `IRequestContext` (controller overrides, pipeline handlers, actions),
565
+ * whose `scope` is the FLAT projection (`{ organizationId?, userId?, orgRoles? }`
566
+ * lifted from `metadata._scope` — see `RequestScopeProjection`)
567
+ */
568
+ interface ScopeFirstCtxRequest {
569
+ scope?: RequestScope | {
570
+ organizationId?: string | undefined;
571
+ userId?: string | undefined;
572
+ orgRoles?: string[] | undefined;
573
+ } | undefined;
574
+ /** `ArcHeaders` — readonly, repeated headers may arrive as arrays. Fastify's `IncomingHttpHeaders` assigns cleanly. */
575
+ headers?: ArcHeaders | undefined;
576
+ }
577
+ /**
578
+ * The ONE shared actor/org derivation for resource handlers and module
579
+ * packages (`arc-*` fleet) — scope-FIRST, per the canonical precedence:
580
+ *
581
+ * 1. `request.scope` (arc's validated `RequestScope`, set by auth adapters):
582
+ * `userId` (human) or `clientId` (service) → `actorId`; `getOrgId(scope)`
583
+ * → `organizationId`.
584
+ * 2. The `x-organization-id` header — ONLY when the scope carries no org.
585
+ * This is the bridge for custom-auth/public routes where arc leaves scope
586
+ * unpopulated; it never overrides a validated scope.
587
+ *
588
+ * Replaces per-package hand-rolled clones of
589
+ * `req.user?._id ?? req.user?.id ?? headers['x-organization-id']` — that
590
+ * pattern re-implements (and drifts from) arc's canonical resolution. A
591
+ * package's `ctxOf(req)` should call this for the shared
592
+ * `{ actorId, organizationId }` and only ADD its kernel-specific fields
593
+ * (`actorKind` / `correlationId` / `roles` / `idempotencyKey`).
594
+ *
595
+ * Structural request shape — works on a raw `FastifyRequest` AND on arc's
596
+ * `IRequestContext` (see {@link ScopeFirstCtxRequest}: the discriminated
597
+ * `RequestScope` and the flat controller projection are both understood).
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * import { scopeFirstCtx } from '@classytic/arc/scope';
602
+ *
603
+ * // Guaranteed actor (kernel context requires one):
604
+ * const { actorId, organizationId } = scopeFirstCtx(req, { fallbackActorId: 'system' });
605
+ *
606
+ * // Optional actor (package applies its own policy):
607
+ * const ctx = scopeFirstCtx(req);
608
+ * if (ctx.actorId) audit.record({ actor: ctx.actorId });
609
+ * ```
610
+ */
611
+ declare function scopeFirstCtx(req: ScopeFirstCtxRequest, options: ScopeFirstCtxOptions & {
612
+ fallbackActorId: string;
613
+ }): {
614
+ actorId: string;
615
+ organizationId?: string;
616
+ };
617
+ declare function scopeFirstCtx(req: ScopeFirstCtxRequest, options?: ScopeFirstCtxOptions): ScopeFirstCtx;
520
618
  /** Default public scope — used as initial decoration value */
521
619
  declare const PUBLIC_SCOPE: Readonly<RequestScope>;
522
620
  /** Default authenticated scope — used when user is logged in but no org */
523
621
  declare const AUTHENTICATED_SCOPE: Readonly<RequestScope>;
524
622
  //#endregion
525
- export { isMember as C, requireOrgId as D, requireClientId as E, requireTeamId as O, isElevated as S, isService as T, getTenantFromRequest as _, getAncestorOrgIds as a, hasOrgAccess as b, getMandate as c, getOrgRoles as d, getRequestScope as f, getTeamId as g, getServiceScopes as h, RequestScope as i, requireUserId as k, getOrgContext as l, getScopeContextMap as m, Mandate as n, getClientId as o, getScopeContext as p, PUBLIC_SCOPE as r, getDPoPJkt as s, AUTHENTICATED_SCOPE as t, getOrgId as u, getUserId as v, isOrgInScope as w, isAuthenticated as x, getUserRoles as y };
623
+ export { requireOrgId as A, hasOrgAccess as C, isOrgInScope as D, isMember as E, getHeader as F, requireSingleHeaderValue as I, requireUserId as M, scopeFirstCtx as N, isService as O, ArcHeaders as P, getUserRoles as S, isElevated as T, getScopeContextMap as _, ScopeFirstCtx as a, getTenantFromRequest as b, getAncestorOrgIds as c, getMandate as d, getOrgContext as f, getScopeContext as g, getRequestScope as h, RequestScope as i, requireTeamId as j, requireClientId as k, getClientId as l, getOrgRoles as m, Mandate as n, ScopeFirstCtxOptions as o, getOrgId as p, PUBLIC_SCOPE as r, ScopeFirstCtxRequest as s, AUTHENTICATED_SCOPE as t, getDPoPJkt as u, getServiceScopes as v, isAuthenticated as w, getUserId as x, getTeamId as y };
@@ -1,5 +1,99 @@
1
- import { i as RequestScope } from "./types-DH0GupI7.mjs";
1
+ import { i as RequestScope } from "./types-DjC-KUCz.mjs";
2
2
  import { FastifyRequest } from "fastify";
3
+ //#region src/types/base.d.ts
4
+ declare module "fastify" {
5
+ interface FastifyRequest {
6
+ /** Request scope — set by auth adapter, read by permissions/presets/guards */
7
+ scope: RequestScope;
8
+ /**
9
+ * Current user — set by auth adapter (Better Auth, JWT, custom).
10
+ * `undefined` on public routes (`auth: false`) or unauthenticated requests.
11
+ * Guard with `if (request.user)` on routes that allow anonymous access.
12
+ *
13
+ * Kept as required (not `user?`) because `@fastify/jwt` declares it
14
+ * as required — declaration merges must have identical modifiers.
15
+ * The `| undefined` in the type achieves the same DX.
16
+ */
17
+ user: Record<string, unknown> | undefined;
18
+ /** Policy-injected query filters (e.g. ownership, org-scoping) */
19
+ _policyFilters?: Record<string, unknown>;
20
+ /** Field mask — fields to include/exclude in responses */
21
+ fieldMask?: {
22
+ include?: string[];
23
+ exclude?: string[];
24
+ };
25
+ /** Arbitrary policy metadata for downstream consumers */
26
+ policyMetadata?: Record<string, unknown>;
27
+ /** Document loaded by policy middleware for ownership checks */
28
+ document?: unknown;
29
+ /** Ownership check context (field name + user field) */
30
+ _ownershipCheck?: Record<string, unknown>;
31
+ }
32
+ }
33
+ type AnyRecord = Record<string, unknown>;
34
+ /** MongoDB ObjectId — accepts string or any object with a `toString()` (e.g. mongoose ObjectId). */
35
+ type ObjectId = string | {
36
+ toString(): string;
37
+ };
38
+ /**
39
+ * Flexible user type that accepts any object with id/_id properties.
40
+ * The actual user structure is defined by your app's auth system.
41
+ */
42
+ type UserLike = UserBase & {
43
+ /** User email (optional) */
44
+ email?: string;
45
+ };
46
+ interface UserOrganization {
47
+ userId: string;
48
+ organizationId: string;
49
+ [key: string]: unknown;
50
+ }
51
+ interface JWTPayload {
52
+ sub: string;
53
+ [key: string]: unknown;
54
+ }
55
+ /**
56
+ * Typed Fastify request with Arc decorations. Use in `raw: true` handlers
57
+ * instead of `(req as any).user`.
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * import type { ArcRequest } from '@classytic/arc';
62
+ *
63
+ * handler: async (req: ArcRequest, reply) => {
64
+ * req.user?.id; // typed
65
+ * req.scope.organizationId; // typed (when member)
66
+ * req.signal; // AbortSignal (Fastify 5)
67
+ * }
68
+ * ```
69
+ */
70
+ type ArcRequest = FastifyRequest & {
71
+ scope: RequestScope;
72
+ user: Record<string, unknown> | undefined;
73
+ signal: AbortSignal;
74
+ };
75
+ //#endregion
76
+ //#region src/utils/userHelpers.d.ts
77
+ /**
78
+ * Extract a user ID from a user object. Accepts `id` or `_id` — returns
79
+ * `undefined` when neither is present. Used by arc's controllers to
80
+ * populate `createdBy` / `updatedBy` fields and for cache scoping.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { getUserId } from '@classytic/arc/utils';
85
+ * const uid = getUserId(request.user);
86
+ * ```
87
+ */
88
+ declare function getUserId(user: UserLike | null | undefined): string | undefined;
89
+ /** Normalize a raw role value from Better Auth, JWT, or a custom identity provider. */
90
+ declare function normalizeRoles(value: unknown): string[];
91
+ /** Extract normalized roles from a user-like object. */
92
+ declare function getUserRoles(user: {
93
+ role?: unknown;
94
+ roles?: unknown;
95
+ } | null | undefined): string[];
96
+ //#endregion
3
97
  //#region src/permissions/types.d.ts
4
98
  /**
5
99
  * User base interface - minimal shape Arc expects
@@ -24,8 +118,6 @@ interface UserBase {
24
118
  * Normalize a raw role value (string, comma-separated string, or array) into a string[].
25
119
  * Shared low-level helper used by both getUserRoles() and the Better Auth adapter.
26
120
  */
27
- declare function normalizeRoles(value: unknown): string[];
28
- declare function getUserRoles(user: UserBase | null | undefined): string[];
29
121
  /**
30
122
  * Context passed to permission check functions
31
123
  */
@@ -195,4 +287,4 @@ interface PermissionCheckMeta {
195
287
  };
196
288
  }
197
289
  //#endregion
198
- export { getUserRoles as a, UserBase as i, PermissionContext as n, normalizeRoles as o, PermissionResult as r, PermissionCheck as t };
290
+ export { getUserId as a, AnyRecord as c, ObjectId as d, UserLike as f, UserBase as i, ArcRequest as l, PermissionContext as n, getUserRoles as o, UserOrganization as p, PermissionResult as r, normalizeRoles as s, PermissionCheck as t, JWTPayload as u };
@@ -1,4 +1,4 @@
1
- import { V as ResourceDefinition } from "./index-BBsvQt7U.mjs";
1
+ import { V as ResourceDefinition } from "./index-DPRH0Cme.mjs";
2
2
  import { z } from "zod";
3
3
  //#region src/integrations/mcp/types.d.ts
4
4
  /** Behavioral hints for MCP clients (tool annotations per MCP spec) */
@@ -1,5 +1,6 @@
1
- import { i as RequestScope } from "../types-DH0GupI7.mjs";
2
- import { n as PermissionContext, t as PermissionCheck } from "../types-Csxga7d3.mjs";
1
+ import { i as RequestScope } from "../types-DjC-KUCz.mjs";
2
+ import { n as PermissionContext, t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
3
+ import { UsageBucket, UsageStore, usagePeriod } from "@classytic/repo-core/usage";
3
4
  import { FastifyPluginAsync, FastifyRequest } from "fastify";
4
5
  //#region src/usage/requireQuota.d.ts
5
6
  interface QuotaOptions {
@@ -28,56 +29,6 @@ interface QuotaOptions {
28
29
  }
29
30
  declare function requireQuota(options: QuotaOptions): PermissionCheck;
30
31
  //#endregion
31
- //#region src/usage/stores/interface.d.ts
32
- /**
33
- * Usage store contract — period-bucketed counters per actor.
34
- *
35
- * CANONICAL HOME: `@classytic/repo-core/usage` (>=0.10) owns this
36
- * contract for the ecosystem — kits implement it there without ever
37
- * depending on arc (`@classytic/mongokit/usage`, ...). This file is a
38
- * deliberate STRUCTURAL MIRROR of that shape — same trick as
39
- * `ScheduleLockLike` vs repo-core's `LockAdapter` — so arc's emitted
40
- * declarations carry no repo-core@>=0.10 subpath reference and the
41
- * peer floor stays honest. Any conforming adapter assigns directly.
42
- *
43
- * The deliberately tiny surface (one increment, one read) is what makes
44
- * every backend trivial: memory (dev/tests), Redis (`HINCRBY`), or any kit
45
- * (Mongo `$inc` upsert) implements it in a handful of lines. Aggregation
46
- * happens IN the store — arc never buffers counters in process memory
47
- * beyond the built-in memory store, so multi-replica deployments stay
48
- * correct as long as the store is shared.
49
- *
50
- * This is the HORIZONTAL usage layer (per-actor, per-period platform
51
- * accounting for quotas/plans/billing). Itemized vertical records — e.g.
52
- * `@classytic/arc-ai/usage`'s per-run AI token/cost tracking — keep their
53
- * own richer stores and can sink into this one (`kind: 'ai.tokens'`).
54
- */
55
- /** One counter cell: (actor, period, kind). */
56
- interface UsageBucket {
57
- /** Who consumed — org / user / client id, or an IP-derived fallback. */
58
- actor: string;
59
- /** Aggregation period, `YYYY-MM` (see `usagePeriod()`). */
60
- period: string;
61
- /** Namespaced counter, dot-separated: `api.requests`, `ai.tokens`, `storage.bytes`. */
62
- kind: string;
63
- }
64
- interface UsageStore {
65
- /** Store name for diagnostics (e.g. 'memory', 'redis', 'mongo'). */
66
- readonly name: string;
67
- /** Add `amount` to the bucket's counter, creating it at 0 first. MUST be atomic per bucket. */
68
- increment(bucket: UsageBucket, amount: number): Promise<void> | void;
69
- /** Every counter for an actor in a period: `{ 'api.requests': 40231, ... }`. */
70
- summary(actor: string, period: string): Promise<Record<string, number>>;
71
- /** Optional cleanup hook (connections, timers). */
72
- close?(): Promise<void>;
73
- }
74
- /**
75
- * Canonical period key for a date — calendar month, UTC: `2026-07`.
76
- * Monthly is the billing-native granularity; stores that want finer
77
- * windows can shard internally without changing the contract.
78
- */
79
- declare function usagePeriod(date?: Date): string;
80
- //#endregion
81
32
  //#region src/usage/stores/memory.d.ts
82
33
  declare class MemoryUsageStore implements UsageStore {
83
34
  #private;
Binary file
@@ -0,0 +1,29 @@
1
+ //#region src/utils/userHelpers.ts
2
+ /**
3
+ * Extract a user ID from a user object. Accepts `id` or `_id` — returns
4
+ * `undefined` when neither is present. Used by arc's controllers to
5
+ * populate `createdBy` / `updatedBy` fields and for cache scoping.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { getUserId } from '@classytic/arc/utils';
10
+ * const uid = getUserId(request.user);
11
+ * ```
12
+ */
13
+ function getUserId(user) {
14
+ if (!user) return void 0;
15
+ const id = user.id ?? user._id;
16
+ return id ? String(id) : void 0;
17
+ }
18
+ /** Normalize a raw role value from Better Auth, JWT, or a custom identity provider. */
19
+ function normalizeRoles(value) {
20
+ if (Array.isArray(value)) return value.map((role) => String(role).trim()).filter(Boolean);
21
+ if (typeof value === "string" && value.length > 0) return value.split(",").map((role) => role.trim()).filter(Boolean);
22
+ return [];
23
+ }
24
+ /** Extract normalized roles from a user-like object. */
25
+ function getUserRoles(user) {
26
+ return user ? normalizeRoles(user.role ?? user.roles) : [];
27
+ }
28
+ //#endregion
29
+ export { getUserRoles as n, normalizeRoles as r, getUserId as t };