@classytic/arc 2.11.3 → 2.13.1

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 (185) hide show
  1. package/README.md +27 -18
  2. package/dist/{BaseController-swXruJ2_.mjs → BaseController-DX_T-bDB.mjs} +388 -423
  3. package/dist/EventTransport-CT_52aWU.d.mts +34 -0
  4. package/dist/EventTransport-DLWoUMHy.mjs +103 -0
  5. package/dist/{QueryCache-DOBNHBE0.d.mts → QueryCache-D41bfdBB.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-DkAeAuTX.mjs → ResourceRegistry-CTERg_2x.mjs} +139 -66
  7. package/dist/audit/index.d.mts +2 -2
  8. package/dist/audit/index.mjs +1 -1
  9. package/dist/auth/audit.d.mts +199 -0
  10. package/dist/auth/audit.mjs +288 -0
  11. package/dist/auth/index.d.mts +5 -5
  12. package/dist/auth/index.mjs +117 -191
  13. package/dist/auth/redis-session.d.mts +1 -1
  14. package/dist/{betterAuthOpenApi-DwxtK3uG.mjs → betterAuthOpenApi--M_i87dQ.mjs} +1 -1
  15. package/dist/buildHandler-olo-gt94.mjs +610 -0
  16. package/dist/cache/index.d.mts +3 -3
  17. package/dist/cache/index.mjs +3 -3
  18. package/dist/cli/commands/describe.d.mts +89 -13
  19. package/dist/cli/commands/describe.mjs +56 -2
  20. package/dist/cli/commands/docs.mjs +2 -2
  21. package/dist/cli/commands/generate.mjs +147 -48
  22. package/dist/cli/commands/init.d.mts +13 -0
  23. package/dist/cli/commands/init.mjs +237 -112
  24. package/dist/cli/commands/introspect.mjs +8 -1
  25. package/dist/context/index.mjs +1 -1
  26. package/dist/core/index.d.mts +3 -3
  27. package/dist/core/index.mjs +5 -5
  28. package/dist/core-D72ia0EH.mjs +1399 -0
  29. package/dist/{createActionRouter-u3ql2EDo.mjs → createActionRouter-CEvzKcy8.mjs} +7 -20
  30. package/dist/createAggregationRouter-CyecOxnO.mjs +114 -0
  31. package/dist/{createApp-BFxtdKy6.mjs → createApp-XX2-N0Yd.mjs} +31 -27
  32. package/dist/defineEvent-D5h7EvAx.mjs +188 -0
  33. package/dist/docs/index.d.mts +2 -2
  34. package/dist/docs/index.mjs +2 -2
  35. package/dist/{elevation-DOFoxoDs.mjs → elevation-DgoeTyfX.mjs} +1 -1
  36. package/dist/errorHandler-Bk-AGhkU.mjs +174 -0
  37. package/dist/errorHandler-DFr45ZG4.d.mts +45 -0
  38. package/dist/errors-j4aJm1Wg.mjs +184 -0
  39. package/dist/{eventPlugin-KrFIQ097.mjs → eventPlugin-CaKTYkYM.mjs} +35 -137
  40. package/dist/{eventPlugin-CUNjYYRY.d.mts → eventPlugin-qXpqTebY.d.mts} +57 -7
  41. package/dist/events/index.d.mts +164 -5
  42. package/dist/events/index.mjs +133 -209
  43. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  44. package/dist/events/transports/redis-stream-entry.mjs +204 -31
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +2 -2
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-C8Y0XLAu.d.mts → fields-COhcH3fk.d.mts} +23 -2
  49. package/dist/hooks/index.d.mts +1 -1
  50. package/dist/hooks/index.mjs +1 -1
  51. package/dist/idempotency/index.d.mts +3 -3
  52. package/dist/idempotency/index.mjs +1 -20
  53. package/dist/idempotency/redis.d.mts +1 -1
  54. package/dist/idempotency/redis.mjs +1 -1
  55. package/dist/{index-BYCqHCVu.d.mts → index-BTqLEvhu.d.mts} +164 -4
  56. package/dist/{index-6u4_Gg6G.d.mts → index-BtW7qYwa.d.mts} +661 -281
  57. package/dist/{index-BdXnTPRj.d.mts → index-Ds61mrJE.d.mts} +50 -4
  58. package/dist/{index-DdQ3O9Pg.d.mts → index-Dz5IKsrE.d.mts} +360 -219
  59. package/dist/index.d.mts +6 -7
  60. package/dist/index.mjs +9 -10
  61. package/dist/integrations/event-gateway.d.mts +2 -2
  62. package/dist/integrations/event-gateway.mjs +1 -1
  63. package/dist/integrations/index.d.mts +2 -2
  64. package/dist/integrations/mcp/index.d.mts +2 -2
  65. package/dist/integrations/mcp/index.mjs +1 -1
  66. package/dist/integrations/mcp/testing.d.mts +1 -1
  67. package/dist/integrations/mcp/testing.mjs +1 -1
  68. package/dist/integrations/streamline.d.mts +60 -11
  69. package/dist/integrations/streamline.mjs +75 -85
  70. package/dist/integrations/websocket-redis.d.mts +1 -1
  71. package/dist/integrations/websocket.d.mts +1 -1
  72. package/dist/integrations/websocket.mjs +2 -8
  73. package/dist/middleware/index.d.mts +1 -1
  74. package/dist/middleware/index.mjs +2 -2
  75. package/dist/migrations/index.d.mts +23 -3
  76. package/dist/migrations/index.mjs +0 -7
  77. package/dist/{multipartBody-CvTR1Un6.mjs → multipartBody-BOvVSVCD.mjs} +11 -8
  78. package/dist/{openapi-BGUn7Ki1.mjs → openapi-CiOMVW1p.mjs} +143 -13
  79. package/dist/org/index.d.mts +2 -2
  80. package/dist/org/index.mjs +1 -1
  81. package/dist/permissions/index.d.mts +3 -3
  82. package/dist/permissions/index.mjs +3 -3
  83. package/dist/{permissions-gd_aUWrR.mjs → permissions-ohQyv50e.mjs} +404 -176
  84. package/dist/{pipe-DVoIheVC.mjs → pipe-Zr0KXjQe.mjs} +1 -1
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +18 -33
  88. package/dist/plugins/index.mjs +33 -13
  89. package/dist/plugins/response-cache.mjs +1 -1
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/presets/filesUpload.d.mts +5 -5
  93. package/dist/presets/filesUpload.mjs +6 -9
  94. package/dist/presets/index.d.mts +1 -1
  95. package/dist/presets/index.mjs +1 -1
  96. package/dist/presets/multiTenant.d.mts +1 -1
  97. package/dist/presets/multiTenant.mjs +2 -2
  98. package/dist/presets/search.d.mts +2 -2
  99. package/dist/presets/search.mjs +6 -8
  100. package/dist/{presets-Z7P5w4gF.mjs → presets-BbkjdPeH.mjs} +6 -28
  101. package/dist/{queryCachePlugin-BUXBSm4F.d.mts → queryCachePlugin-CqMdLI2-.d.mts} +2 -2
  102. package/dist/{queryCachePlugin-Bq6bO6vc.mjs → queryCachePlugin-m1XsgAIJ.mjs} +3 -3
  103. package/dist/{redis-Cm1gnRDf.d.mts → redis-DiMkdHEl.d.mts} +1 -1
  104. package/dist/redis-stream-D6HzR1Z_.d.mts +232 -0
  105. package/dist/registry/index.d.mts +1 -1
  106. package/dist/registry/index.mjs +2 -2
  107. package/dist/{replyHelpers-ByllIXXV.mjs → replyHelpers-CK-FNO8E.mjs} +3 -21
  108. package/dist/{resourceToTools-ByZpgjeH.mjs → resourceToTools-C5coh64w.mjs} +224 -71
  109. package/dist/{routerShared-BqLRb5l7.mjs → routerShared-D6_fEGHh.mjs} +40 -36
  110. package/dist/{schemaIR-BlG9bY7v.mjs → schemaIR-7Vl611Qs.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +100 -30
  112. package/dist/schemas/index.mjs +86 -29
  113. package/dist/scim/index.d.mts +264 -0
  114. package/dist/scim/index.mjs +963 -0
  115. package/dist/scope/index.d.mts +3 -3
  116. package/dist/scope/index.mjs +4 -4
  117. package/dist/{sse-V7aXc3bW.mjs → sse-Bz-5ZeTt.mjs} +1 -1
  118. package/dist/{store-helpers-BhrzxvyQ.mjs → store-helpers-BkIN9-vu.mjs} +1 -1
  119. package/dist/testing/index.d.mts +2 -8
  120. package/dist/testing/index.mjs +16 -24
  121. package/dist/testing/storageContract.d.mts +1 -1
  122. package/dist/types/index.d.mts +4 -4
  123. package/dist/types/storage.d.mts +1 -1
  124. package/dist/{types-BH7dEGvU.d.mts → types-BvqwCCSx.d.mts} +77 -29
  125. package/dist/{types-tgR4Pt8F.d.mts → types-CTYvcwHe.d.mts} +195 -1
  126. package/dist/{types-AOD8fxIw.mjs → types-C_s5moIu.mjs} +117 -1
  127. package/dist/{types-9beEMe25.d.mts → types-DQHFc8PM.d.mts} +1 -1
  128. package/dist/utils/index.d.mts +2 -2
  129. package/dist/utils/index.mjs +5 -5
  130. package/dist/{utils-CcYTj09l.mjs → utils-_h9B3c57.mjs} +1269 -1334
  131. package/dist/{versioning-M9lNLhO8.d.mts → versioning-DTTvc80y.d.mts} +1 -1
  132. package/package.json +24 -34
  133. package/skills/arc/SKILL.md +521 -785
  134. package/skills/arc/references/agent-auth.md +238 -0
  135. package/skills/arc/references/api-reference.md +187 -0
  136. package/skills/arc/references/auth.md +354 -7
  137. package/skills/arc/references/enterprise-auth.md +94 -0
  138. package/skills/arc/references/events.md +8 -6
  139. package/skills/arc/references/mcp.md +2 -2
  140. package/skills/arc/references/multi-tenancy.md +11 -2
  141. package/skills/arc/references/production.md +10 -9
  142. package/skills/arc/references/scim.md +247 -0
  143. package/skills/arc/references/testing.md +1 -1
  144. package/skills/arc-code-review/SKILL.md +141 -0
  145. package/skills/arc-code-review/references/anti-patterns.md +911 -0
  146. package/skills/arc-code-review/references/arc-cheatsheet.md +380 -0
  147. package/skills/arc-code-review/references/migration-recipes.md +700 -0
  148. package/skills/arc-code-review/references/mongokit-migration.md +386 -0
  149. package/skills/arc-code-review/references/scaffolding.md +230 -0
  150. package/skills/arc-code-review/references/severity.md +127 -0
  151. package/dist/EventTransport-CfVEGaEl.d.mts +0 -293
  152. package/dist/adapters/index.d.mts +0 -3
  153. package/dist/adapters/index.mjs +0 -2
  154. package/dist/adapters-D0tT2Tyo.mjs +0 -949
  155. package/dist/auth/mongoose.d.mts +0 -191
  156. package/dist/auth/mongoose.mjs +0 -73
  157. package/dist/core-DnUsRpuX.mjs +0 -1049
  158. package/dist/errorHandler-BQm8ZxTK.mjs +0 -173
  159. package/dist/errorHandler-Co3lnVmJ.d.mts +0 -114
  160. package/dist/errors-D5c-5BJL.mjs +0 -232
  161. package/dist/index-BbMrcvGp.d.mts +0 -362
  162. package/dist/redis-stream-CM8TXTix.d.mts +0 -110
  163. /package/dist/{HookSystem-CGsMd6oK.mjs → HookSystem-Iiebom92.mjs} +0 -0
  164. /package/dist/{actionPermissions-sUUKDhtP.mjs → actionPermissions-CyUkQu6O.mjs} +0 -0
  165. /package/dist/{caching-CheW3m-S.mjs → caching-SM8gghN6.mjs} +0 -0
  166. /package/dist/{constants-BhY1OHoH.mjs → constants-Cxde4rpC.mjs} +0 -0
  167. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-BXOWoGCF.d.mts} +0 -0
  168. /package/dist/{externalPaths-Bapitwvd.d.mts → externalPaths-BD5nw6St.d.mts} +0 -0
  169. /package/dist/{interface-CkkWm5uR.d.mts → interface-DfLGcus7.d.mts} +0 -0
  170. /package/dist/{interface-Da0r7Lna.d.mts → interface-beEtJyWM.d.mts} +0 -0
  171. /package/dist/{keys-CARyUjiR.mjs → keys-CGcCbNyu.mjs} +0 -0
  172. /package/dist/{loadResources-CPpkyKfM.mjs → loadResources-DBMQg_Aj.mjs} +0 -0
  173. /package/dist/{memory-DikHSvWa.mjs → memory-UBydS5ku.mjs} +0 -0
  174. /package/dist/{metrics-Csh4nsvv.mjs → metrics-Qnvwc-LQ.mjs} +0 -0
  175. /package/dist/{pluralize-BneOJkpi.mjs → pluralize-DQgqgifU.mjs} +0 -0
  176. /package/dist/{registry-D63ee7fl.mjs → registry-I-ogLgL9.mjs} +0 -0
  177. /package/dist/{requestContext-C5XeK3VA.mjs → requestContext-SSaaTgW8.mjs} +0 -0
  178. /package/dist/{schemaConverter-B0oKLuqI.mjs → schemaConverter-De34B1ZG.mjs} +0 -0
  179. /package/dist/{sessionManager-D-oNWHz3.d.mts → sessionManager-C4Le_UB3.d.mts} +0 -0
  180. /package/dist/{storage-BwGQXUpd.d.mts → storage-Dfzt4VTl.d.mts} +0 -0
  181. /package/dist/{tracing-DokiEsuz.d.mts → tracing-QJVprktp.d.mts} +0 -0
  182. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-BzkXkvVv.mjs} +0 -0
  183. /package/dist/{types-DV9WDfeg.mjs → types-D57iXYb8.mjs} +0 -0
  184. /package/dist/{versioning-CGPjkqAg.mjs → versioning-BUrT5aP4.mjs} +0 -0
  185. /package/dist/{websocket-CyJ1VIFI.d.mts → websocket-ChC2rqe1.d.mts} +0 -0
@@ -0,0 +1,34 @@
1
+ import { DeadLetteredEvent, DomainEvent, DomainEvent as DomainEvent$1, EventHandler, EventHandler as EventHandler$1, EventLogger, EventLogger as EventLogger$1, EventTransport, EventTransport as EventTransport$1, PublishManyResult } from "@classytic/primitives/events";
2
+
3
+ //#region src/events/EventTransport.d.ts
4
+ interface MemoryEventTransportOptions {
5
+ /** Logger for error/warning messages (default: console) */
6
+ logger?: EventLogger;
7
+ }
8
+ /**
9
+ * In-memory event transport (default).
10
+ *
11
+ * Events are delivered synchronously within the process. Not suitable for
12
+ * multi-instance deployments — pair with `RedisEventTransport` /
13
+ * `RedisStreamTransport` (subpath imports) for those.
14
+ */
15
+ declare class MemoryEventTransport implements EventTransport {
16
+ readonly name = "memory";
17
+ private handlers;
18
+ private logger;
19
+ constructor(options?: MemoryEventTransportOptions);
20
+ publish(event: DomainEvent): Promise<void>;
21
+ /**
22
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
23
+ *
24
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
25
+ * this with a single batched network call. Memory transport has nothing to
26
+ * batch, so we just loop — the loop still returns a proper result map so
27
+ * `EventOutbox.relay` can exercise the batched code path in tests.
28
+ */
29
+ publishMany(events: readonly DomainEvent[]): Promise<PublishManyResult>;
30
+ subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
31
+ close(): Promise<void>;
32
+ }
33
+ //#endregion
34
+ export { EventTransport$1 as a, EventLogger$1 as i, DomainEvent$1 as n, MemoryEventTransport as o, EventHandler$1 as r, MemoryEventTransportOptions as s, DeadLetteredEvent as t };
@@ -0,0 +1,103 @@
1
+ import { createEvent, matchEventPattern } from "@classytic/primitives/events";
2
+ //#region src/events/EventTransport.ts
3
+ /**
4
+ * Internal arc events module — concrete in-memory transport.
5
+ *
6
+ * **Public type contracts moved out (arc 2.12).** `EventMeta`, `DomainEvent`,
7
+ * `EventHandler`, `EventLogger`, `EventTransport`, `DeadLetteredEvent`,
8
+ * `PublishManyResult`, plus the `createEvent` / `createChildEvent` /
9
+ * `matchEventPattern` helpers, are now owned by `@classytic/primitives/events`.
10
+ * Two reasons:
11
+ *
12
+ * 1. The shapes ARE the cross-package contract. arc, arc-next, mongokit's
13
+ * audit-log plugin, future kits and downstream services all share them.
14
+ * Owning them in `primitives` (pure types, zero runtime, zero deps)
15
+ * eliminates the "manually mirror in two places" problem the previous
16
+ * `primitives/src/events.ts` header explicitly called out.
17
+ * 2. Inverting the dependency direction lets future packages (sqlitekit
18
+ * audit, billing services) consume the contract without depending on
19
+ * arc's HTTP-coupled stack.
20
+ *
21
+ * Hosts MUST import the types from primitives directly:
22
+ *
23
+ * ```typescript
24
+ * import type {
25
+ * EventMeta, DomainEvent, EventHandler, EventTransport,
26
+ * DeadLetteredEvent, PublishManyResult,
27
+ * } from '@classytic/primitives/events';
28
+ * import { createEvent, createChildEvent } from '@classytic/primitives/events';
29
+ * ```
30
+ *
31
+ * arc's `events/index.ts` barrel does NOT re-export these — by design, so
32
+ * that consumer code is forced into the canonical import path and the org's
33
+ * "no re-exports" rule holds at every public surface.
34
+ *
35
+ * What stays here:
36
+ * - `MemoryEventTransport` — the in-memory `EventTransport` implementation
37
+ * used as arc's default transport. Its handler-set state is process-local;
38
+ * wrong layer for primitives.
39
+ * - `MemoryEventTransportOptions` — its options shape.
40
+ *
41
+ * Inside arc, files that previously imported types from `./EventTransport.js`
42
+ * keep working: this file re-exports them from primitives for arc's own
43
+ * call sites. That re-export is an internal refactor convenience — the
44
+ * public `events/index.ts` barrel stays clean.
45
+ */
46
+ /**
47
+ * In-memory event transport (default).
48
+ *
49
+ * Events are delivered synchronously within the process. Not suitable for
50
+ * multi-instance deployments — pair with `RedisEventTransport` /
51
+ * `RedisStreamTransport` (subpath imports) for those.
52
+ */
53
+ var MemoryEventTransport = class {
54
+ name = "memory";
55
+ handlers = /* @__PURE__ */ new Map();
56
+ logger;
57
+ constructor(options) {
58
+ this.logger = options?.logger ?? console;
59
+ }
60
+ async publish(event) {
61
+ const allHandlers = /* @__PURE__ */ new Set();
62
+ for (const [pattern, handlers] of this.handlers) if (matchEventPattern(pattern, event.type)) for (const h of handlers) allHandlers.add(h);
63
+ for (const handler of allHandlers) try {
64
+ await handler(event);
65
+ } catch (err) {
66
+ this.logger.error(`[EventTransport] Handler error for ${event.type}:`, err);
67
+ }
68
+ }
69
+ /**
70
+ * Reference `publishMany` implementation — delegates to `publish()` in order.
71
+ *
72
+ * Production transports (Kafka, Redis pipeline, SQS batch) should override
73
+ * this with a single batched network call. Memory transport has nothing to
74
+ * batch, so we just loop — the loop still returns a proper result map so
75
+ * `EventOutbox.relay` can exercise the batched code path in tests.
76
+ */
77
+ async publishMany(events) {
78
+ const results = /* @__PURE__ */ new Map();
79
+ for (const event of events) try {
80
+ await this.publish(event);
81
+ results.set(event.meta.id, null);
82
+ } catch (err) {
83
+ results.set(event.meta.id, err instanceof Error ? err : new Error(String(err)));
84
+ }
85
+ return results;
86
+ }
87
+ async subscribe(pattern, handler) {
88
+ if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
89
+ this.handlers.get(pattern)?.add(handler);
90
+ return () => {
91
+ const set = this.handlers.get(pattern);
92
+ if (set) {
93
+ set.delete(handler);
94
+ if (set.size === 0) this.handlers.delete(pattern);
95
+ }
96
+ };
97
+ }
98
+ async close() {
99
+ this.handlers.clear();
100
+ }
101
+ };
102
+ //#endregion
103
+ export { createEvent as n, MemoryEventTransport as t };
@@ -1,4 +1,4 @@
1
- import { r as CacheStore } from "./interface-Da0r7Lna.mjs";
1
+ import { r as CacheStore } from "./interface-beEtJyWM.mjs";
2
2
 
3
3
  //#region src/cache/QueryCache.d.ts
4
4
  /** Metadata wrapper stored in CacheStore */
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "./constants-BhY1OHoH.mjs";
1
+ import "./constants-Cxde4rpC.mjs";
2
2
  //#region src/registry/ResourceRegistry.ts
3
3
  /**
4
4
  * Resource Registry
@@ -46,7 +46,7 @@ var ResourceRegistry = class {
46
46
  registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
47
47
  disableDefaultRoutes: resource.disableDefaultRoutes,
48
48
  updateMethod: resource.updateMethod,
49
- disabledRoutes: resource.disabledRoutes,
49
+ disabledRoutes: resource.disabledRoutes ? [...resource.disabledRoutes] : void 0,
50
50
  openApiSchemas: options.openApiSchemas,
51
51
  fieldPermissions: extractFieldPermissions(resource.fields),
52
52
  pipelineSteps: extractPipelineSteps(resource.pipe),
@@ -63,6 +63,18 @@ var ResourceRegistry = class {
63
63
  mcp: entry.mcp
64
64
  };
65
65
  }) : void 0,
66
+ aggregations: resource.aggregations ? Object.entries(resource.aggregations).map(([name, entry]) => ({
67
+ name,
68
+ summary: entry.summary,
69
+ description: entry.description,
70
+ permissions: entry.permissions,
71
+ groupBy: entry.groupBy,
72
+ measures: stringifyMeasureMap(entry.measures),
73
+ lookupAliases: (entry.lookups ?? []).map((l) => l.as ?? l.from),
74
+ requireDateRange: entry.requireDateRange,
75
+ requireFilters: entry.requireFilters,
76
+ mcp: entry.mcp
77
+ })) : void 0,
66
78
  plugin: resource.toPlugin()
67
79
  };
68
80
  this._resources.set(resource.name, entry);
@@ -100,6 +112,11 @@ var ResourceRegistry = class {
100
112
  }
101
113
  /**
102
114
  * Get registry statistics
115
+ *
116
+ * `totalRoutes` is derived from `enumerateRoutes()` — single source of
117
+ * truth shared with `getIntrospection()` and consistent with what
118
+ * OpenAPI / Fastify actually mount. New route sources (e.g. v2.13
119
+ * aggregations) light up here automatically.
103
120
  */
104
121
  getStats() {
105
122
  const resources = this.getAll();
@@ -109,82 +126,110 @@ var ResourceRegistry = class {
109
126
  totalResources: resources.length,
110
127
  byModule: this._groupBy(resources, "module"),
111
128
  presetUsage: presetCounts,
112
- totalRoutes: resources.reduce((sum, r) => {
113
- const actionsCount = (r.actions?.length ?? 0) > 0 ? 1 : 0;
114
- if (r.disableDefaultRoutes) return sum + (r.customRoutes?.length ?? 0) + actionsCount;
115
- const disabledSet = new Set(r.disabledRoutes ?? []);
116
- let defaultCount = CRUD_OPERATIONS.filter((route) => !disabledSet.has(route)).length;
117
- if (!disabledSet.has("update") && r.updateMethod === "both") defaultCount += 1;
118
- return sum + defaultCount + (r.customRoutes?.length ?? 0) + actionsCount;
119
- }, 0),
129
+ totalRoutes: resources.reduce((sum, r) => sum + this.enumerateRoutes(r).length, 0),
120
130
  totalEvents: resources.reduce((sum, r) => sum + (r.events?.length ?? 0), 0)
121
131
  };
122
132
  }
123
133
  /**
124
134
  * Get full introspection data
135
+ *
136
+ * Routes come from `enumerateRoutes()` so consumers see the complete
137
+ * surface — CRUD + custom + actions + aggregations — and match what
138
+ * `getStats()` counts.
125
139
  */
126
140
  getIntrospection() {
127
141
  return {
128
- resources: this.getAll().map((r) => {
129
- const disabledSet = new Set(r.disabledRoutes ?? []);
130
- const updateMethod = r.updateMethod ?? "PATCH";
131
- const defaultRoutes = r.disableDefaultRoutes ? [] : [
132
- ...!disabledSet.has("list") ? [{
133
- method: "GET",
134
- path: r.prefix,
135
- operation: "list"
136
- }] : [],
137
- ...!disabledSet.has("get") ? [{
138
- method: "GET",
139
- path: `${r.prefix}/:id`,
140
- operation: "get"
141
- }] : [],
142
- ...!disabledSet.has("create") ? [{
143
- method: "POST",
144
- path: r.prefix,
145
- operation: "create"
146
- }] : [],
147
- ...!disabledSet.has("update") ? updateMethod === "both" ? [{
148
- method: "PUT",
149
- path: `${r.prefix}/:id`,
150
- operation: "update"
151
- }, {
152
- method: "PATCH",
153
- path: `${r.prefix}/:id`,
154
- operation: "update"
155
- }] : [{
156
- method: updateMethod,
157
- path: `${r.prefix}/:id`,
158
- operation: "update"
159
- }] : [],
160
- ...!disabledSet.has("delete") ? [{
161
- method: "DELETE",
162
- path: `${r.prefix}/:id`,
163
- operation: "delete"
164
- }] : []
165
- ];
166
- return {
167
- name: r.name,
168
- displayName: r.displayName,
169
- prefix: r.prefix,
170
- module: r.module,
171
- presets: r.presets,
172
- permissions: r.permissions,
173
- routes: [...defaultRoutes, ...r.customRoutes?.map((ar) => ({
174
- method: ar.method,
175
- path: `${r.prefix}${ar.path}`,
176
- operation: ar.operation ?? (typeof ar.handler === "string" ? ar.handler : "custom"),
177
- handler: typeof ar.handler === "string" ? ar.handler : void 0,
178
- summary: ar.summary
179
- })) ?? []],
180
- events: r.events
181
- };
182
- }),
142
+ resources: this.getAll().map((r) => ({
143
+ name: r.name,
144
+ displayName: r.displayName,
145
+ prefix: r.prefix,
146
+ module: r.module,
147
+ presets: r.presets,
148
+ permissions: r.permissions,
149
+ routes: this.enumerateRoutes(r),
150
+ events: r.events
151
+ })),
183
152
  stats: this.getStats(),
184
153
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
185
154
  };
186
155
  }
187
156
  /**
157
+ * Single source of truth for "what routes does this resource expose?".
158
+ *
159
+ * Enumerates every wire route the resource will mount on Fastify:
160
+ * - default CRUD (respecting `disabledRoutes` + `updateMethod`)
161
+ * - host-declared `customRoutes` (alias: `routes`)
162
+ * - the unified `POST /:id/action` endpoint when `actions` is set
163
+ * - one `GET /:resource/aggregations/:name` per declared aggregation
164
+ *
165
+ * Both `getStats()` and `getIntrospection()` consume this list, so a
166
+ * new route source (e.g. future webhook routes) only has to be added
167
+ * here — the count and the introspection contract update together.
168
+ * Mirrors the same set of paths emitted by `docs/openapi.ts`.
169
+ */
170
+ enumerateRoutes(r) {
171
+ const routes = [];
172
+ if (!r.disableDefaultRoutes) {
173
+ const disabled = new Set(r.disabledRoutes ?? []);
174
+ const updateMethod = r.updateMethod ?? "PATCH";
175
+ if (!disabled.has("list")) routes.push({
176
+ method: "GET",
177
+ path: r.prefix,
178
+ operation: "list"
179
+ });
180
+ if (!disabled.has("get")) routes.push({
181
+ method: "GET",
182
+ path: `${r.prefix}/:id`,
183
+ operation: "get"
184
+ });
185
+ if (!disabled.has("create")) routes.push({
186
+ method: "POST",
187
+ path: r.prefix,
188
+ operation: "create"
189
+ });
190
+ if (!disabled.has("update")) if (updateMethod === "both") {
191
+ routes.push({
192
+ method: "PUT",
193
+ path: `${r.prefix}/:id`,
194
+ operation: "update"
195
+ });
196
+ routes.push({
197
+ method: "PATCH",
198
+ path: `${r.prefix}/:id`,
199
+ operation: "update"
200
+ });
201
+ } else routes.push({
202
+ method: updateMethod,
203
+ path: `${r.prefix}/:id`,
204
+ operation: "update"
205
+ });
206
+ if (!disabled.has("delete")) routes.push({
207
+ method: "DELETE",
208
+ path: `${r.prefix}/:id`,
209
+ operation: "delete"
210
+ });
211
+ }
212
+ for (const ar of r.customRoutes ?? []) routes.push({
213
+ method: ar.method,
214
+ path: `${r.prefix}${ar.path}`,
215
+ operation: ar.operation ?? (typeof ar.handler === "string" ? ar.handler : "custom"),
216
+ handler: typeof ar.handler === "string" ? ar.handler : void 0,
217
+ summary: ar.summary
218
+ });
219
+ if (r.actions && r.actions.length > 0) routes.push({
220
+ method: "POST",
221
+ path: `${r.prefix}/:id/action`,
222
+ operation: "action"
223
+ });
224
+ for (const agg of r.aggregations ?? []) routes.push({
225
+ method: "GET",
226
+ path: `${r.prefix}/aggregations/${agg.name}`,
227
+ operation: `aggregation:${agg.name}`,
228
+ summary: agg.summary
229
+ });
230
+ return routes;
231
+ }
232
+ /**
188
233
  * Freeze registry (prevent further registrations)
189
234
  */
190
235
  freeze() {
@@ -261,5 +306,33 @@ function extractPipelineSteps(pipe) {
261
306
  operations: s.operations ? [...s.operations] : void 0
262
307
  }));
263
308
  }
309
+ /**
310
+ * Stringify a measure map for the registry. Object IR (`{ op: 'sum',
311
+ * field: 'price' }`) collapses to its op-tag form (`'sum:price'`) so
312
+ * the OpenAPI / MCP / describe layers can render docs without
313
+ * re-implementing IR walking. Bare `count` (no field) stays `'count'`.
314
+ *
315
+ * Exported so the CLI describe command renders the same string form —
316
+ * single source of truth for "how a measure looks to tooling".
317
+ */
318
+ function stringifyMeasureMap(measures) {
319
+ const out = {};
320
+ for (const [alias, m] of Object.entries(measures)) {
321
+ if (typeof m === "string") {
322
+ out[alias] = m;
323
+ continue;
324
+ }
325
+ if (m.op === "count" && !("field" in m && m.field)) {
326
+ out[alias] = "count";
327
+ continue;
328
+ }
329
+ if ("field" in m && m.field) {
330
+ out[alias] = `${m.op}:${m.field}`;
331
+ continue;
332
+ }
333
+ out[alias] = m.op;
334
+ }
335
+ return out;
336
+ }
264
337
  //#endregion
265
- export { ResourceRegistry as t };
338
+ export { stringifyMeasureMap as n, ResourceRegistry as t };
@@ -1,6 +1,6 @@
1
- import { jn as RepositoryLike } from "../index-6u4_Gg6G.mjs";
2
- import { d as UserBase } from "../fields-C8Y0XLAu.mjs";
1
+ import { d as UserBase } from "../fields-COhcH3fk.mjs";
3
2
  import { FastifyPluginAsync } from "fastify";
3
+ import { RepositoryLike } from "@classytic/repo-core/adapter";
4
4
 
5
5
  //#region src/audit/stores/interface.d.ts
6
6
  type AuditAction = "create" | "update" | "delete" | "restore" | "custom";
@@ -50,7 +50,7 @@ function repositoryAsAuditStore(repository) {
50
50
  page,
51
51
  limit
52
52
  });
53
- return (Array.isArray(result) ? result : result.docs ?? []).map((d) => ({
53
+ return (Array.isArray(result) ? result : result.data ?? []).map((d) => ({
54
54
  id: String(d[idField] ?? ""),
55
55
  resource: d.resource ?? "",
56
56
  documentId: d.documentId ?? "",
@@ -0,0 +1,199 @@
1
+ import { FastifyInstance } from "fastify";
2
+
3
+ //#region src/auth/audit.d.ts
4
+ /**
5
+ * Canonical auth event names emitted by the bridge. Hosts pattern-match
6
+ * with glob (`session.*`, `mfa.*`, `*.failed`) when listing `events`.
7
+ *
8
+ * Names mirror BA's database-hook + endpoint-hook taxonomy. Not exhaustive —
9
+ * unknown events still flow through when matched, just with the BA-supplied
10
+ * name verbatim. Use this list for autocomplete-friendly defaults.
11
+ */
12
+ type AuthEventName = "session.create" | "session.delete" | "session.update" | "user.create" | "user.update" | "user.delete" | "mfa.enroll" | "mfa.verify" | "mfa.failed" | "mfa.disable" | "password.reset.request" | "password.reset.complete" | "email.verify" | "org.create" | "org.delete" | "org.invite.create" | "org.invite.accept" | "org.invite.reject" | "org.member.add" | "org.member.remove" | "org.member.role.update" | "apikey.create" | "apikey.delete" | "apikey.failed" | "endpoint.before" | "endpoint.after" | "endpoint.error";
13
+ /**
14
+ * Resolved auth event ready for audit. The bridge constructs this from
15
+ * BA's hook context, then forwards to `app.audit.custom('auth', subjectId,
16
+ * event.name, event.payload)`.
17
+ */
18
+ interface AuthEvent {
19
+ /** Canonical name (see {@link AuthEventName}). */
20
+ name: string;
21
+ /**
22
+ * Subject id — the user / session / org / invite id this event is about.
23
+ * Empty string when the event is pre-creation (e.g. `mfa.failed` before
24
+ * the session exists); audit rows accept empty `documentId`.
25
+ */
26
+ subjectId: string;
27
+ /** User performing the action (when known); audit infers from request when omitted. */
28
+ userId?: string;
29
+ /** Organization context (when known). */
30
+ organizationId?: string;
31
+ /** Free-form payload — provider name, IP, user-agent, MFA method, etc. */
32
+ payload?: Record<string, unknown>;
33
+ }
34
+ /**
35
+ * Options for `wireBetterAuthAudit`.
36
+ */
37
+ interface WireBetterAuthAuditOptions {
38
+ /**
39
+ * Glob patterns selecting which events to audit. `*` matches a single
40
+ * segment (`session.*` matches `session.create` but not `session.create.foo`),
41
+ * `**` matches deeply. Defaults to `['session.*', 'user.create', 'user.delete']`
42
+ * — the SOC2/HIPAA minimum. Pass `['**']` to audit everything.
43
+ */
44
+ events?: readonly string[];
45
+ /**
46
+ * Custom event-name resolver — invoked when the bridge can't classify
47
+ * an endpoint hook firing. Return a string (used as the audit `action`)
48
+ * to keep it; return `null` to drop. Default behaviour: emits
49
+ * `endpoint.before` / `endpoint.after` / `endpoint.error` with the
50
+ * matched path in `payload.path`.
51
+ */
52
+ resolveEndpointEvent?: (phase: "before" | "after" | "error", ctx: {
53
+ path?: string;
54
+ method?: string;
55
+ error?: unknown;
56
+ }) => string | null;
57
+ /**
58
+ * Maximum number of events to buffer before `attach(app)` is called.
59
+ * Older events are dropped FIFO once full. Default 1000 — enough for
60
+ * boot-time flow even on slow Fastify init paths.
61
+ */
62
+ bufferSize?: number;
63
+ /**
64
+ * Optional pre-flight transform — runs before the audit row is written.
65
+ * Return `null` to drop the event; otherwise the returned event is
66
+ * forwarded. Use to redact tokens, add custom payload fields, or
67
+ * map BA-specific event names to your own taxonomy.
68
+ */
69
+ transform?: (event: AuthEvent) => AuthEvent | null | Promise<AuthEvent | null>;
70
+ }
71
+ /**
72
+ * Result of `wireBetterAuthAudit` — duck-typed BA `hooks` + `databaseHooks`
73
+ * config plus an `attach(app)` method to connect the live audit logger.
74
+ */
75
+ interface BetterAuthAuditBridge {
76
+ /**
77
+ * Spread into `betterAuth({ hooks })`. BA's top-level `hooks.before` /
78
+ * `hooks.after` slots take a single async function each (not an array).
79
+ * The bridge collapses its internal endpoint classifier into one dispatcher
80
+ * per phase. Returns `{}` so BA's `runAfterHooks` reads `.headers` / `.response`
81
+ * on a real object instead of crashing on undefined. For plugin-author array
82
+ * form, see {@link asPluginHooks}.
83
+ */
84
+ hooks: {
85
+ before: (ctx: BetterAuthHookContext) => Promise<Record<string, never>>;
86
+ after: (ctx: BetterAuthHookContext) => Promise<Record<string, never>>;
87
+ };
88
+ /**
89
+ * Plugin-form hooks — array of `{ matcher, handler }` entries suitable for
90
+ * `betterAuth({ plugins: [{ id, hooks: bridge.asPluginHooks() }] })`. Use
91
+ * when you're writing a BA plugin and want the audit dispatch to ride along
92
+ * with your plugin's own hooks instead of occupying the top-level slot.
93
+ */
94
+ asPluginHooks(): {
95
+ before: BetterAuthHookEntry[];
96
+ after: BetterAuthHookEntry[];
97
+ };
98
+ /** Spread into `betterAuth({ databaseHooks })`. */
99
+ databaseHooks: {
100
+ user: {
101
+ create: {
102
+ after: (user: {
103
+ id?: string;
104
+ }) => Promise<void>;
105
+ };
106
+ update: {
107
+ after: (user: {
108
+ id?: string;
109
+ }) => Promise<void>;
110
+ };
111
+ delete?: {
112
+ after: (user: {
113
+ id?: string;
114
+ }) => Promise<void>;
115
+ };
116
+ };
117
+ session: {
118
+ create: {
119
+ after: (session: SessionLike) => Promise<void>;
120
+ };
121
+ update?: {
122
+ after: (session: SessionLike) => Promise<void>;
123
+ };
124
+ delete?: {
125
+ after: (session: SessionLike) => Promise<void>;
126
+ };
127
+ };
128
+ };
129
+ /**
130
+ * Connect a live Fastify instance after boot. Buffered events from the
131
+ * BA construction phase are flushed in order; subsequent events stream
132
+ * through the connected `app.audit.custom(...)` directly.
133
+ */
134
+ attach(app: FastifyInstance): void;
135
+ /**
136
+ * Manual emit — for hosts that need to record auth events outside BA's
137
+ * hook surface (e.g. webhook signature failures, custom MFA flows).
138
+ */
139
+ emit(event: AuthEvent): void;
140
+ /**
141
+ * Observability counters — surface to your metrics backend. Counters are
142
+ * cumulative since bridge construction; reset when `wireBetterAuthAudit`
143
+ * is called again. Useful for Prometheus exporters that scrape periodically.
144
+ */
145
+ getStats(): {
146
+ /** Events buffered during pre-`attach` phase that were dropped due to `bufferSize` overflow. */droppedFromBuffer: number; /** Events that reached `app.audit.custom(...)` but threw (audit store failure). */
147
+ dispatchFailures: number; /** Total `dispatch` calls — useful to verify BA is actually invoking the bridge's hooks. */
148
+ dispatchAttempts: number; /** Events currently buffered awaiting `attach`. */
149
+ pendingBuffered: number;
150
+ };
151
+ }
152
+ /**
153
+ * Single BA hook entry — duck-typed to avoid pinning a BA major version.
154
+ * The shape mirrors `better-auth`'s public `hooks.{before,after}` API.
155
+ */
156
+ interface BetterAuthHookEntry {
157
+ matcher: (ctx: BetterAuthHookContext) => boolean;
158
+ /**
159
+ * Returns a (possibly empty) object so BA's `runBeforeHooks` /
160
+ * `runAfterHooks` can read `.headers` / `.response` off the result without
161
+ * the `Cannot read properties of undefined` crash that hits when handlers
162
+ * return `void`. The bridge always returns `{}` because it's a side-effect
163
+ * dispatcher — no header rewrites or response substitution.
164
+ */
165
+ handler: (ctx: BetterAuthHookContext) => Promise<Record<string, unknown>> | Record<string, unknown>;
166
+ }
167
+ /** Subset of BA's hook context the bridge actually reads. */
168
+ interface BetterAuthHookContext {
169
+ path?: string;
170
+ method?: string;
171
+ body?: Record<string, unknown>;
172
+ headers?: Record<string, unknown>;
173
+ context?: {
174
+ session?: {
175
+ user?: {
176
+ id?: string;
177
+ };
178
+ activeOrganizationId?: string;
179
+ };
180
+ request?: {
181
+ ip?: string;
182
+ headers?: Record<string, string | undefined>;
183
+ };
184
+ };
185
+ returned?: unknown;
186
+ error?: unknown;
187
+ }
188
+ /** Subset of BA's session shape the bridge reads from databaseHooks. */
189
+ interface SessionLike {
190
+ id?: string;
191
+ userId?: string;
192
+ activeOrganizationId?: string;
193
+ ipAddress?: string;
194
+ userAgent?: string;
195
+ impersonatedBy?: string;
196
+ }
197
+ declare function wireBetterAuthAudit(opts?: WireBetterAuthAuditOptions): BetterAuthAuditBridge;
198
+ //#endregion
199
+ export { AuthEvent, AuthEventName, BetterAuthAuditBridge, BetterAuthHookContext, BetterAuthHookEntry, WireBetterAuthAuditOptions, wireBetterAuthAudit };