@classytic/arc 2.2.5 → 2.4.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 (174) hide show
  1. package/README.md +187 -18
  2. package/bin/arc.js +11 -3
  3. package/dist/BaseController-CkM5dUh_.mjs +1031 -0
  4. package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
  5. package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
  6. package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
  7. package/dist/adapters/index.d.mts +3 -5
  8. package/dist/adapters/index.mjs +2 -3
  9. package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
  10. package/dist/audit/index.d.mts +4 -7
  11. package/dist/audit/index.mjs +2 -29
  12. package/dist/audit/mongodb.d.mts +1 -4
  13. package/dist/audit/mongodb.mjs +2 -3
  14. package/dist/auth/index.d.mts +7 -9
  15. package/dist/auth/index.mjs +65 -63
  16. package/dist/auth/redis-session.d.mts +1 -1
  17. package/dist/auth/redis-session.mjs +1 -2
  18. package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
  19. package/dist/cache/index.d.mts +23 -23
  20. package/dist/cache/index.mjs +4 -6
  21. package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
  22. package/dist/chunk-BpYLSNr0.mjs +14 -0
  23. package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
  24. package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
  25. package/dist/cli/commands/describe.mjs +24 -7
  26. package/dist/cli/commands/docs.mjs +6 -7
  27. package/dist/cli/commands/doctor.d.mts +10 -0
  28. package/dist/cli/commands/doctor.mjs +156 -0
  29. package/dist/cli/commands/generate.mjs +66 -17
  30. package/dist/cli/commands/init.mjs +315 -45
  31. package/dist/cli/commands/introspect.mjs +2 -4
  32. package/dist/cli/index.d.mts +1 -10
  33. package/dist/cli/index.mjs +4 -153
  34. package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
  35. package/dist/core/index.d.mts +3 -5
  36. package/dist/core/index.mjs +5 -4
  37. package/dist/core-C1XCMtqM.mjs +185 -0
  38. package/dist/{createApp-BKHSl2nT.mjs → createApp-ByWNRsZj.mjs} +65 -36
  39. package/dist/{defineResource-DO9ONe_D.mjs → defineResource-D9aY5Cy6.mjs} +154 -1165
  40. package/dist/discovery/index.mjs +37 -5
  41. package/dist/docs/index.d.mts +6 -9
  42. package/dist/docs/index.mjs +3 -21
  43. package/dist/dynamic/index.d.mts +93 -0
  44. package/dist/dynamic/index.mjs +122 -0
  45. package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
  46. package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
  47. package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
  48. package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
  49. package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
  50. package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
  51. package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
  52. package/dist/events/index.d.mts +72 -7
  53. package/dist/events/index.mjs +216 -4
  54. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  55. package/dist/events/transports/redis-stream-entry.mjs +19 -7
  56. package/dist/events/transports/redis.d.mts +1 -1
  57. package/dist/events/transports/redis.mjs +3 -4
  58. package/dist/factory/index.d.mts +23 -9
  59. package/dist/factory/index.mjs +48 -3
  60. package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
  61. package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
  62. package/dist/hooks/index.d.mts +1 -3
  63. package/dist/hooks/index.mjs +2 -3
  64. package/dist/idempotency/index.d.mts +5 -5
  65. package/dist/idempotency/index.mjs +3 -7
  66. package/dist/idempotency/mongodb.d.mts +1 -1
  67. package/dist/idempotency/mongodb.mjs +4 -5
  68. package/dist/idempotency/redis.d.mts +1 -1
  69. package/dist/idempotency/redis.mjs +2 -5
  70. package/dist/{fastifyAdapter-CyAA2zlB.d.mts → index-BL8CaQih.d.mts} +56 -57
  71. package/dist/index-Diqcm14c.d.mts +369 -0
  72. package/dist/{prisma-xjhMEq_S.d.mts → index-yhxyjqNb.d.mts} +4 -5
  73. package/dist/index.d.mts +100 -105
  74. package/dist/index.mjs +85 -58
  75. package/dist/integrations/event-gateway.d.mts +1 -1
  76. package/dist/integrations/event-gateway.mjs +8 -4
  77. package/dist/integrations/index.d.mts +4 -2
  78. package/dist/integrations/index.mjs +1 -1
  79. package/dist/integrations/jobs.d.mts +2 -2
  80. package/dist/integrations/jobs.mjs +63 -14
  81. package/dist/integrations/mcp/index.d.mts +219 -0
  82. package/dist/integrations/mcp/index.mjs +572 -0
  83. package/dist/integrations/mcp/testing.d.mts +53 -0
  84. package/dist/integrations/mcp/testing.mjs +104 -0
  85. package/dist/integrations/streamline.mjs +39 -19
  86. package/dist/integrations/webhooks.d.mts +56 -0
  87. package/dist/integrations/webhooks.mjs +139 -0
  88. package/dist/integrations/websocket-redis.d.mts +46 -0
  89. package/dist/integrations/websocket-redis.mjs +50 -0
  90. package/dist/integrations/websocket.d.mts +68 -2
  91. package/dist/integrations/websocket.mjs +96 -13
  92. package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
  93. package/dist/interface-DGmPxakH.d.mts +2213 -0
  94. package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
  95. package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
  96. package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
  97. package/dist/metrics-Csh4nsvv.mjs +224 -0
  98. package/dist/migrations/index.mjs +3 -7
  99. package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
  100. package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
  101. package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
  102. package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
  103. package/dist/org/index.d.mts +12 -14
  104. package/dist/org/index.mjs +92 -119
  105. package/dist/org/types.d.mts +2 -2
  106. package/dist/org/types.mjs +1 -1
  107. package/dist/permissions/index.d.mts +4 -278
  108. package/dist/permissions/index.mjs +4 -579
  109. package/dist/permissions-CA5zg0yK.mjs +751 -0
  110. package/dist/plugins/index.d.mts +104 -107
  111. package/dist/plugins/index.mjs +203 -313
  112. package/dist/plugins/response-cache.mjs +4 -69
  113. package/dist/plugins/tracing-entry.d.mts +1 -1
  114. package/dist/plugins/tracing-entry.mjs +24 -11
  115. package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
  116. package/dist/policies/index.d.mts +2 -2
  117. package/dist/policies/index.mjs +80 -83
  118. package/dist/presets/index.d.mts +26 -19
  119. package/dist/presets/index.mjs +2 -142
  120. package/dist/presets/multiTenant.d.mts +1 -4
  121. package/dist/presets/multiTenant.mjs +4 -6
  122. package/dist/presets-C9QXJV1u.mjs +422 -0
  123. package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
  124. package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
  125. package/dist/queryParser-CgCtsjti.mjs +352 -0
  126. package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
  127. package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
  128. package/dist/registry/index.d.mts +1 -4
  129. package/dist/registry/index.mjs +3 -4
  130. package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
  131. package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
  132. package/dist/resourceToTools-B6ZN9Ing.mjs +489 -0
  133. package/dist/rpc/index.d.mts +90 -0
  134. package/dist/rpc/index.mjs +248 -0
  135. package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
  136. package/dist/schemas/index.d.mts +30 -30
  137. package/dist/schemas/index.mjs +4 -6
  138. package/dist/scope/index.d.mts +13 -2
  139. package/dist/scope/index.mjs +18 -5
  140. package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
  141. package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
  142. package/dist/testing/index.d.mts +551 -567
  143. package/dist/testing/index.mjs +1744 -1799
  144. package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
  145. package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
  146. package/dist/types/index.d.mts +4 -946
  147. package/dist/types/index.mjs +2 -4
  148. package/dist/types-BJmgxNbF.d.mts +275 -0
  149. package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
  150. package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
  151. package/dist/{types-DMSBMkaZ.d.mts → types-Dt0-AI6E.d.mts} +85 -27
  152. package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
  153. package/dist/utils/index.d.mts +255 -352
  154. package/dist/utils/index.mjs +7 -6
  155. package/dist/utils-Dc0WhlIl.mjs +594 -0
  156. package/dist/versioning-BzfeHmhj.mjs +37 -0
  157. package/package.json +46 -12
  158. package/skills/arc/SKILL.md +506 -0
  159. package/skills/arc/references/auth.md +250 -0
  160. package/skills/arc/references/events.md +272 -0
  161. package/skills/arc/references/integrations.md +385 -0
  162. package/skills/arc/references/mcp.md +386 -0
  163. package/skills/arc/references/production.md +610 -0
  164. package/skills/arc/references/testing.md +183 -0
  165. package/dist/audited-CGdLiSlE.mjs +0 -140
  166. package/dist/chunk-C7Uep-_p.mjs +0 -20
  167. package/dist/circuitBreaker-DYhWBW_D.mjs +0 -1096
  168. package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
  169. package/dist/interface-DZYNK9bb.d.mts +0 -1112
  170. package/dist/presets-BTeYbw7h.d.mts +0 -57
  171. package/dist/presets-CeFtfDR8.mjs +0 -119
  172. /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
  173. /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
  174. /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
@@ -1,7 +1,6 @@
1
- import { t as __exportAll } from "./chunk-C7Uep-_p.mjs";
2
- import { t as requestContext } from "./requestContext-xi6OKBL-.mjs";
1
+ import { t as __exportAll } from "./chunk-BpYLSNr0.mjs";
2
+ import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
3
3
  import fp from "fastify-plugin";
4
-
5
4
  //#region src/events/EventTransport.ts
6
5
  /**
7
6
  * In-memory event transport (default)
@@ -21,7 +20,7 @@ var MemoryEventTransport = class {
21
20
  const patternHandlers = /* @__PURE__ */ new Set();
22
21
  for (const [pattern, handlers] of this.handlers.entries()) if (pattern.endsWith(".*")) {
23
22
  const prefix = pattern.slice(0, -2);
24
- if (event.type.startsWith(prefix + ".")) handlers.forEach((h) => patternHandlers.add(h));
23
+ if (event.type.startsWith(`${prefix}.`)) for (const h of handlers) patternHandlers.add(h);
25
24
  }
26
25
  const allHandlers = new Set([
27
26
  ...exactHandlers,
@@ -36,9 +35,13 @@ var MemoryEventTransport = class {
36
35
  }
37
36
  async subscribe(pattern, handler) {
38
37
  if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
39
- this.handlers.get(pattern).add(handler);
38
+ this.handlers.get(pattern)?.add(handler);
40
39
  return () => {
41
- this.handlers.get(pattern)?.delete(handler);
40
+ const set = this.handlers.get(pattern);
41
+ if (set) {
42
+ set.delete(handler);
43
+ if (set.size === 0) this.handlers.delete(pattern);
44
+ }
42
45
  };
43
46
  }
44
47
  async close() {
@@ -59,7 +62,6 @@ function createEvent(type, payload, meta) {
59
62
  }
60
63
  };
61
64
  }
62
-
63
65
  //#endregion
64
66
  //#region src/events/retry.ts
65
67
  /**
@@ -130,32 +132,20 @@ function createDeadLetterPublisher(events) {
130
132
  function sleep(ms) {
131
133
  return new Promise((resolve) => setTimeout(resolve, ms));
132
134
  }
133
-
134
135
  //#endregion
135
136
  //#region src/events/eventPlugin.ts
136
- /**
137
- * Event Plugin
138
- *
139
- * Integrates event transport with Fastify.
140
- * Defaults to in-memory transport; configure durable transport for production.
141
- *
142
- * @example
143
- * // Development (in-memory)
144
- * await fastify.register(eventPlugin);
145
- *
146
- * // Production (Redis)
147
- * await fastify.register(eventPlugin, {
148
- * transport: new RedisEventTransport({ url: process.env.REDIS_URL }),
149
- * });
150
- */
151
137
  var eventPlugin_exports = /* @__PURE__ */ __exportAll({
152
138
  default: () => eventPlugin_default,
153
139
  eventPlugin: () => eventPlugin
154
140
  });
155
141
  const eventPlugin = async (fastify, opts = {}) => {
156
- const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError } = opts;
142
+ const { transport = new MemoryEventTransport(), logEvents = false, failOpen = true, retry: retryOpts, deadLetterQueue: dlqOpts, wal, onPublish, onPublishError, registry, validateMode: rawValidateMode } = opts;
143
+ const validateMode = rawValidateMode ?? (registry ? "warn" : "off");
157
144
  fastify.decorate("events", {
158
145
  publish: async (type, payload, meta) => {
146
+ if (!type || typeof type !== "string") throw new Error("[Arc Events] Event type must be a non-empty string");
147
+ if (type.startsWith("$") && type !== "$deadLetter") throw new Error(`[Arc Events] Event type '${type}' uses reserved '$' prefix`);
148
+ if (type.length > 256) throw new Error("[Arc Events] Event type exceeds 256 characters");
159
149
  const store = requestContext.get();
160
150
  const event = createEvent(type, payload, {
161
151
  ...store?.requestId && !meta?.correlationId ? { correlationId: store.requestId } : {},
@@ -166,6 +156,14 @@ const eventPlugin = async (fastify, opts = {}) => {
166
156
  eventId: event.meta.id,
167
157
  correlationId: event.meta.correlationId
168
158
  }, "Publishing event");
159
+ if (registry && validateMode !== "off") {
160
+ const result = registry.validate(type, payload);
161
+ if (!result.valid) {
162
+ const msg = `[Arc Events] Event '${type}' payload validation failed: ${result.errors?.join("; ")}`;
163
+ if (validateMode === "reject") throw new Error(msg);
164
+ fastify.log?.warn?.(msg);
165
+ }
166
+ }
169
167
  try {
170
168
  if (wal) await wal.save(event);
171
169
  await transport.publish(event);
@@ -204,7 +202,8 @@ const eventPlugin = async (fastify, opts = {}) => {
204
202
  return () => {};
205
203
  }
206
204
  },
207
- transportName: transport.name
205
+ transportName: transport.name,
206
+ registry
208
207
  });
209
208
  fastify.addHook("onClose", async () => {
210
209
  try {
@@ -224,6 +223,5 @@ var eventPlugin_default = fp(eventPlugin, {
224
223
  name: "arc-events",
225
224
  fastify: "5.x"
226
225
  });
227
-
228
226
  //#endregion
229
- export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createEvent as o, createDeadLetterPublisher as r, eventPlugin as t };
227
+ export { MemoryEventTransport as a, withRetry as i, eventPlugin_exports as n, createEvent as o, createDeadLetterPublisher as r, eventPlugin as t };
@@ -1,6 +1,77 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-BkUDYZEb.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "./EventTransport-wc5hSLik.mjs";
2
2
  import { FastifyPluginAsync } from "fastify";
3
3
 
4
+ //#region src/events/defineEvent.d.ts
5
+ interface EventSchema {
6
+ type: "object";
7
+ properties?: Record<string, {
8
+ type?: string;
9
+ [key: string]: unknown;
10
+ }>;
11
+ required?: string[];
12
+ [key: string]: unknown;
13
+ }
14
+ interface EventDefinitionInput {
15
+ /** Event type name (e.g., 'order.created') */
16
+ name: string;
17
+ /** JSON Schema for payload validation */
18
+ schema?: EventSchema;
19
+ /** Event version for schema evolution (default: 1) */
20
+ version?: number;
21
+ /** Human-readable description */
22
+ description?: string;
23
+ }
24
+ interface EventDefinitionOutput<T = unknown> {
25
+ /** Event type name */
26
+ readonly name: string;
27
+ /** JSON Schema for payload validation */
28
+ readonly schema?: EventSchema;
29
+ /** Event version */
30
+ readonly version: number;
31
+ /** Human-readable description */
32
+ readonly description?: string;
33
+ /** Create a DomainEvent with this type + auto-generated metadata */
34
+ create(payload: T, meta?: Partial<DomainEvent["meta"]>): DomainEvent<T>;
35
+ }
36
+ interface ValidationResult {
37
+ valid: boolean;
38
+ errors?: string[];
39
+ }
40
+ interface EventRegistry {
41
+ /** Register an event definition */
42
+ register(definition: EventDefinitionOutput): void;
43
+ /** Get event definition by name (latest version if no version specified) */
44
+ get(name: string, version?: number): EventDefinitionOutput | undefined;
45
+ /** Get full catalog of registered events */
46
+ catalog(): ReadonlyArray<{
47
+ name: string;
48
+ version: number;
49
+ description?: string;
50
+ schema?: EventSchema;
51
+ }>;
52
+ /** Validate a payload against a registered event's schema */
53
+ validate(name: string, payload: unknown): ValidationResult;
54
+ }
55
+ /**
56
+ * Define a typed event with optional schema validation.
57
+ *
58
+ * @example
59
+ * const OrderCreated = defineEvent({
60
+ * name: 'order.created',
61
+ * schema: { type: 'object', properties: { orderId: { type: 'string' } }, required: ['orderId'] },
62
+ * });
63
+ *
64
+ * const event = OrderCreated.create({ orderId: '123' });
65
+ */
66
+ declare function defineEvent<T = unknown>(input: EventDefinitionInput): EventDefinitionOutput<T>;
67
+ /**
68
+ * Create an event registry for cataloging and validating events.
69
+ *
70
+ * The registry is opt-in — unregistered events pass validation.
71
+ * This allows gradual adoption without breaking existing code.
72
+ */
73
+ declare function createEventRegistry(): EventRegistry;
74
+ //#endregion
4
75
  //#region src/events/retry.d.ts
5
76
  interface RetryOptions {
6
77
  /**
@@ -79,8 +150,16 @@ interface EventPluginOptions {
79
150
  logEvents?: boolean;
80
151
  /**
81
152
  * Fail-open mode for runtime resilience (default: true).
82
- * - true: publish/subscribe/close errors are logged and suppressed
83
- * - false: errors are thrown to caller
153
+ * - true: publish/subscribe/close errors are logged and suppressed — the
154
+ * request still succeeds even if event delivery fails. Safe for analytics
155
+ * and non-critical side effects.
156
+ * - false: errors are thrown to caller — use this for business-critical
157
+ * events where silent loss is unacceptable (e.g. billing, notifications).
158
+ *
159
+ * **Important:** With `failOpen: true` (default), a transport outage will
160
+ * silently drop events while requests continue succeeding. Pair with the
161
+ * `onPublishError` callback to monitor failures, or use `wal` for
162
+ * at-least-once delivery guarantees.
84
163
  */
85
164
  failOpen?: boolean;
86
165
  /**
@@ -109,16 +188,37 @@ interface EventPluginOptions {
109
188
  onPublish?: (event: DomainEvent) => void;
110
189
  /** Callback on publish failure (for metrics/alerting) */
111
190
  onPublishError?: (event: DomainEvent, error: Error) => void;
191
+ /**
192
+ * Event registry for payload validation and introspection.
193
+ * When provided, payloads are validated against registered schemas on publish.
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const registry = createEventRegistry();
198
+ * registry.register(defineEvent({ name: 'order.created', schema: { ... } }));
199
+ *
200
+ * await fastify.register(eventPlugin, { registry, validateMode: 'warn' });
201
+ * ```
202
+ */
203
+ registry?: EventRegistry;
204
+ /**
205
+ * How to handle schema validation failures on publish:
206
+ * - `'warn'` (default when registry is provided): log a warning, still publish
207
+ * - `'reject'`: throw an error, do NOT publish
208
+ * - `'off'`: skip validation entirely (registry is only for introspection)
209
+ */
210
+ validateMode?: "warn" | "reject" | "off";
112
211
  }
113
212
  declare module "fastify" {
114
213
  interface FastifyInstance {
115
214
  events: {
116
215
  /** Publish an event */publish: <T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>) => Promise<void>; /** Subscribe to events */
117
216
  subscribe: (pattern: string, handler: EventHandler) => Promise<() => void>; /** Get transport name */
118
- transportName: string;
217
+ transportName: string; /** Event registry for introspection (undefined when no registry configured) */
218
+ registry?: EventRegistry;
119
219
  };
120
220
  }
121
221
  }
122
222
  declare const eventPlugin: FastifyPluginAsync<EventPluginOptions>;
123
223
  //#endregion
124
- export { withRetry as a, createDeadLetterPublisher as i, eventPlugin as n, RetryOptions as r, EventPluginOptions as t };
224
+ export { withRetry as a, EventRegistry as c, createEventRegistry as d, defineEvent as f, createDeadLetterPublisher as i, EventSchema as l, eventPlugin as n, EventDefinitionInput as o, RetryOptions as r, EventDefinitionOutput as s, EventPluginOptions as t, ValidationResult as u };
@@ -1,7 +1,7 @@
1
- import { a as MemoryEventTransport, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as createEvent, t as DomainEvent } from "../EventTransport-BkUDYZEb.mjs";
2
- import { a as withRetry, i as createDeadLetterPublisher, n as eventPlugin, r as RetryOptions, t as EventPluginOptions } from "../eventPlugin-H6wDDjGO.mjs";
1
+ import { a as MemoryEventTransport, i as EventTransport, n as EventHandler, o as MemoryEventTransportOptions, r as EventLogger, s as createEvent, t as DomainEvent } from "../EventTransport-wc5hSLik.mjs";
2
+ import { a as withRetry, c as EventRegistry, d as createEventRegistry, f as defineEvent, i as createDeadLetterPublisher, l as EventSchema, n as eventPlugin, o as EventDefinitionInput, r as RetryOptions, s as EventDefinitionOutput, t as EventPluginOptions, u as ValidationResult } from "../eventPlugin-iGrSEmwJ.mjs";
3
3
  import { RedisEventTransportOptions, RedisLike } from "./transports/redis.mjs";
4
- import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-CBg0upHI.mjs";
4
+ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis-stream-BW9UKLZM.mjs";
5
5
 
6
6
  //#region src/events/eventTypes.d.ts
7
7
  /**
@@ -24,7 +24,7 @@ import { r as RedisStreamTransportOptions, t as RedisStreamLike } from "../redis
24
24
  /** Suffixes for auto-emitted CRUD events */
25
25
  declare const CRUD_EVENT_SUFFIXES: readonly ["created", "updated", "deleted"];
26
26
  /** Type for CRUD event suffixes */
27
- type CrudEventSuffix = typeof CRUD_EVENT_SUFFIXES[number];
27
+ type CrudEventSuffix = (typeof CRUD_EVENT_SUFFIXES)[number];
28
28
  /**
29
29
  * Build a CRUD event type string.
30
30
  *
@@ -41,13 +41,78 @@ declare const ARC_LIFECYCLE_EVENTS: Readonly<{
41
41
  readonly READY: "arc.ready";
42
42
  }>;
43
43
  /** Type for Arc lifecycle event names */
44
- type ArcLifecycleEvent = typeof ARC_LIFECYCLE_EVENTS[keyof typeof ARC_LIFECYCLE_EVENTS];
44
+ type ArcLifecycleEvent = (typeof ARC_LIFECYCLE_EVENTS)[keyof typeof ARC_LIFECYCLE_EVENTS];
45
45
  /** Cache-specific event types for observability and external triggers */
46
46
  declare const CACHE_EVENTS: Readonly<{
47
47
  /** Emitted when a resource's cache version is bumped */readonly VERSION_BUMPED: "arc.cache.version.bumped"; /** Emitted when a tag version is bumped */
48
48
  readonly TAG_VERSION_BUMPED: "arc.cache.tag.bumped";
49
49
  }>;
50
50
  /** Type for cache event names */
51
- type CacheEvent = typeof CACHE_EVENTS[keyof typeof CACHE_EVENTS];
51
+ type CacheEvent = (typeof CACHE_EVENTS)[keyof typeof CACHE_EVENTS];
52
52
  //#endregion
53
- export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type DomainEvent, type EventHandler, type EventLogger, type EventPluginOptions, type EventTransport, MemoryEventTransport, type MemoryEventTransportOptions, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RetryOptions, createDeadLetterPublisher, createEvent, crudEventType, eventPlugin, withRetry };
53
+ //#region src/events/outbox.d.ts
54
+ interface OutboxStore {
55
+ /** Save event to outbox (called within business transaction) */
56
+ save(event: DomainEvent): Promise<void>;
57
+ /** Get pending (unrelayed) events, ordered FIFO */
58
+ getPending(limit: number): Promise<DomainEvent[]>;
59
+ /** Mark event as successfully relayed */
60
+ acknowledge(eventId: string): Promise<void>;
61
+ /**
62
+ * Purge old acknowledged events (optional, DB-agnostic contract).
63
+ *
64
+ * Arc does **not** ship a concrete implementation — your store owns the
65
+ * cleanup strategy that fits your database:
66
+ *
67
+ * - **MongoDB:** TTL index on `acknowledgedAt` (automatic, zero-code)
68
+ * - **SQL:** Scheduled `DELETE FROM outbox WHERE acknowledged_at < :cutoff`
69
+ * - **Redis:** Key expiry (`EXPIRE`) on acknowledged entries
70
+ *
71
+ * Called by {@link EventOutbox.purge}. If not implemented, cleanup is
72
+ * entirely the app's responsibility via native DB tools.
73
+ *
74
+ * @param olderThanMs - Remove events acknowledged more than this many ms ago
75
+ * @returns Number of purged events
76
+ */
77
+ purge?(olderThanMs: number): Promise<number>;
78
+ }
79
+ interface EventOutboxOptions {
80
+ /** Outbox store for persistence */
81
+ store: OutboxStore;
82
+ /** Transport to relay events to (optional — can relay later) */
83
+ transport?: EventTransport;
84
+ /** Max events per relay batch (default: 100) */
85
+ batchSize?: number;
86
+ }
87
+ declare class EventOutbox {
88
+ private readonly _store;
89
+ private readonly _transport?;
90
+ private readonly _batchSize;
91
+ constructor(opts: EventOutboxOptions);
92
+ /** Store event in outbox (call within your DB transaction) */
93
+ store(event: DomainEvent): Promise<void>;
94
+ /**
95
+ * Relay pending events to transport.
96
+ *
97
+ * Processes events in FIFO order up to `batchSize`. Stops on the first
98
+ * transport failure — remaining events stay pending for the next relay call.
99
+ *
100
+ * @returns Number of successfully published events in this batch
101
+ */
102
+ relay(): Promise<number>;
103
+ /**
104
+ * Purge old acknowledged events from the outbox store.
105
+ * Delegates to `store.purge()` if implemented; no-op otherwise.
106
+ * @param olderThanMs - Remove events acknowledged more than this many ms ago (default: 7 days)
107
+ * @returns Number of purged events, or 0 if store doesn't support purge
108
+ */
109
+ purge(olderThanMs?: number): Promise<number>;
110
+ }
111
+ declare class MemoryOutboxStore implements OutboxStore {
112
+ private events;
113
+ save(event: DomainEvent): Promise<void>;
114
+ getPending(limit: number): Promise<DomainEvent[]>;
115
+ acknowledge(eventId: string): Promise<void>;
116
+ }
117
+ //#endregion
118
+ export { ARC_LIFECYCLE_EVENTS, type ArcLifecycleEvent, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, type CacheEvent, type CrudEventSuffix, type DomainEvent, type EventDefinitionInput, type EventDefinitionOutput, type EventHandler, type EventLogger, EventOutbox, type EventOutboxOptions, type EventPluginOptions, type EventRegistry, type EventSchema, type EventTransport, MemoryEventTransport, type MemoryEventTransportOptions, MemoryOutboxStore, type OutboxStore, type RedisEventTransportOptions, type RedisLike, type RedisStreamLike, type RedisStreamTransportOptions, type RetryOptions, type ValidationResult, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, withRetry };
@@ -1,5 +1,151 @@
1
- import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-BEOvaDqo.mjs";
2
-
1
+ import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-Ba00swHF.mjs";
2
+ //#region src/events/defineEvent.ts
3
+ /**
4
+ * defineEvent — Typed Event Definitions with Optional Schema Validation
5
+ *
6
+ * Provides:
7
+ * 1. defineEvent() — declare an event with name, schema, version, description
8
+ * 2. EventRegistry — catalog of all known events + payload validation
9
+ * 3. .create() helper — build DomainEvent with auto-generated metadata
10
+ *
11
+ * Schema validation uses a minimal JSON Schema subset (type, required, properties)
12
+ * to avoid pulling in AJV as a dependency. For full JSON Schema validation,
13
+ * users can provide their own validator via the registry.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { defineEvent, createEventRegistry } from '@classytic/arc/events';
18
+ *
19
+ * const OrderCreated = defineEvent({
20
+ * name: 'order.created',
21
+ * version: 1,
22
+ * schema: {
23
+ * type: 'object',
24
+ * properties: {
25
+ * orderId: { type: 'string' },
26
+ * total: { type: 'number' },
27
+ * },
28
+ * required: ['orderId', 'total'],
29
+ * },
30
+ * });
31
+ *
32
+ * // Type-safe event creation
33
+ * const event = OrderCreated.create({ orderId: 'o-1', total: 100 });
34
+ * await fastify.events.publish(event.type, event.payload, event.meta);
35
+ *
36
+ * // Registry for introspection + validation
37
+ * const registry = createEventRegistry();
38
+ * registry.register(OrderCreated);
39
+ * const result = registry.validate('order.created', payload);
40
+ * ```
41
+ */
42
+ /**
43
+ * Define a typed event with optional schema validation.
44
+ *
45
+ * @example
46
+ * const OrderCreated = defineEvent({
47
+ * name: 'order.created',
48
+ * schema: { type: 'object', properties: { orderId: { type: 'string' } }, required: ['orderId'] },
49
+ * });
50
+ *
51
+ * const event = OrderCreated.create({ orderId: '123' });
52
+ */
53
+ function defineEvent(input) {
54
+ const { name, schema, version = 1, description } = input;
55
+ return {
56
+ name,
57
+ schema,
58
+ version,
59
+ description,
60
+ create(payload, meta) {
61
+ return createEvent(name, payload, meta);
62
+ }
63
+ };
64
+ }
65
+ /**
66
+ * Create an event registry for cataloging and validating events.
67
+ *
68
+ * The registry is opt-in — unregistered events pass validation.
69
+ * This allows gradual adoption without breaking existing code.
70
+ */
71
+ function createEventRegistry() {
72
+ const definitions = /* @__PURE__ */ new Map();
73
+ function registryKey(name, version) {
74
+ return `${name}:v${version}`;
75
+ }
76
+ return {
77
+ register(definition) {
78
+ const key = registryKey(definition.name, definition.version);
79
+ if (definitions.has(key)) throw new Error(`Event '${definition.name}' v${definition.version} is already registered. Use a different version number for schema evolution.`);
80
+ definitions.set(key, definition);
81
+ },
82
+ get(name, version) {
83
+ if (version !== void 0) return definitions.get(registryKey(name, version));
84
+ let latest;
85
+ let latestVersion = -1;
86
+ for (const def of definitions.values()) if (def.name === name && def.version > latestVersion) {
87
+ latest = def;
88
+ latestVersion = def.version;
89
+ }
90
+ return latest;
91
+ },
92
+ catalog() {
93
+ return Array.from(definitions.values()).map((def) => ({
94
+ name: def.name,
95
+ version: def.version,
96
+ description: def.description,
97
+ schema: def.schema
98
+ }));
99
+ },
100
+ validate(name, payload) {
101
+ let latest;
102
+ let latestVersion = -1;
103
+ for (const def of definitions.values()) if (def.name === name && def.version > latestVersion) {
104
+ latest = def;
105
+ latestVersion = def.version;
106
+ }
107
+ if (!latest) return { valid: true };
108
+ if (!latest.schema) return { valid: true };
109
+ return validatePayload(payload, latest.schema);
110
+ }
111
+ };
112
+ }
113
+ /**
114
+ * Minimal JSON Schema validator — handles the common subset:
115
+ * - type: 'object'
116
+ * - required: string[]
117
+ * - properties with type checks
118
+ *
119
+ * For full JSON Schema validation (patterns, formats, $ref, etc.),
120
+ * use AJV directly or provide a custom validator.
121
+ */
122
+ function validatePayload(payload, schema) {
123
+ const errors = [];
124
+ if (schema.type === "object") {
125
+ if (payload === null || payload === void 0 || typeof payload !== "object" || Array.isArray(payload)) return {
126
+ valid: false,
127
+ errors: ["Payload must be an object"]
128
+ };
129
+ const record = payload;
130
+ if (schema.required) {
131
+ for (const field of schema.required) if (!(field in record) || record[field] === void 0) errors.push(`Missing required field: '${field}'`);
132
+ }
133
+ if (schema.properties) {
134
+ for (const [key, propSchema] of Object.entries(schema.properties)) if (key in record && record[key] !== void 0 && record[key] !== null) {
135
+ const expectedType = propSchema.type;
136
+ if (expectedType) {
137
+ const actualType = Array.isArray(record[key]) ? "array" : typeof record[key];
138
+ if (expectedType !== actualType) errors.push(`Field '${key}': expected ${expectedType}, got ${actualType}`);
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return errors.length === 0 ? { valid: true } : {
144
+ valid: false,
145
+ errors
146
+ };
147
+ }
148
+ //#endregion
3
149
  //#region src/events/eventTypes.ts
4
150
  /**
5
151
  * Event Type Constants and Helpers
@@ -46,6 +192,72 @@ const CACHE_EVENTS = Object.freeze({
46
192
  VERSION_BUMPED: "arc.cache.version.bumped",
47
193
  TAG_VERSION_BUMPED: "arc.cache.tag.bumped"
48
194
  });
49
-
50
195
  //#endregion
51
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, MemoryEventTransport, createDeadLetterPublisher, createEvent, crudEventType, eventPlugin, withRetry };
196
+ //#region src/events/outbox.ts
197
+ /** Default outbox retention — acknowledged events older than this are eligible for purge */
198
+ const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
199
+ var EventOutbox = class {
200
+ _store;
201
+ _transport;
202
+ _batchSize;
203
+ constructor(opts) {
204
+ this._store = opts.store;
205
+ this._transport = opts.transport;
206
+ this._batchSize = opts.batchSize ?? 100;
207
+ }
208
+ /** Store event in outbox (call within your DB transaction) */
209
+ async store(event) {
210
+ await this._store.save(event);
211
+ }
212
+ /**
213
+ * Relay pending events to transport.
214
+ *
215
+ * Processes events in FIFO order up to `batchSize`. Stops on the first
216
+ * transport failure — remaining events stay pending for the next relay call.
217
+ *
218
+ * @returns Number of successfully published events in this batch
219
+ */
220
+ async relay() {
221
+ if (!this._transport) return 0;
222
+ const pending = await this._store.getPending(this._batchSize);
223
+ let relayed = 0;
224
+ for (const event of pending) {
225
+ if (!event.type || !event.meta?.id) {
226
+ if (event.meta?.id) await this._store.acknowledge(event.meta.id);
227
+ continue;
228
+ }
229
+ try {
230
+ await this._transport.publish(event);
231
+ await this._store.acknowledge(event.meta.id);
232
+ relayed++;
233
+ } catch {
234
+ break;
235
+ }
236
+ }
237
+ return relayed;
238
+ }
239
+ /**
240
+ * Purge old acknowledged events from the outbox store.
241
+ * Delegates to `store.purge()` if implemented; no-op otherwise.
242
+ * @param olderThanMs - Remove events acknowledged more than this many ms ago (default: 7 days)
243
+ * @returns Number of purged events, or 0 if store doesn't support purge
244
+ */
245
+ async purge(olderThanMs = DEFAULT_OUTBOX_RETENTION_MS) {
246
+ if (!this._store.purge) return 0;
247
+ return this._store.purge(olderThanMs);
248
+ }
249
+ };
250
+ var MemoryOutboxStore = class {
251
+ events = [];
252
+ async save(event) {
253
+ this.events.push(event);
254
+ }
255
+ async getPending(limit) {
256
+ return this.events.slice(0, limit);
257
+ }
258
+ async acknowledge(eventId) {
259
+ this.events = this.events.filter((e) => e.meta.id !== eventId);
260
+ }
261
+ };
262
+ //#endregion
263
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, MemoryEventTransport, MemoryOutboxStore, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, withRetry };
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-CBg0upHI.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-BW9UKLZM.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -11,6 +11,7 @@ var RedisStreamTransport = class {
11
11
  claimTimeoutMs;
12
12
  deadLetterStream;
13
13
  maxLen;
14
+ maxPayloadBytes;
14
15
  logger;
15
16
  handlers = /* @__PURE__ */ new Map();
16
17
  running = false;
@@ -27,9 +28,12 @@ var RedisStreamTransport = class {
27
28
  this.claimTimeoutMs = options.claimTimeoutMs ?? 3e4;
28
29
  this.deadLetterStream = options.deadLetterStream ?? "arc:events:dlq";
29
30
  this.maxLen = options.maxLen ?? 1e4;
31
+ this.maxPayloadBytes = options.maxPayloadBytes ?? 1e6;
30
32
  this.logger = options.logger ?? console;
31
33
  }
32
34
  async publish(event) {
35
+ const serialized = JSON.stringify(event);
36
+ if (serialized.length > this.maxPayloadBytes) throw new Error(`[RedisStreamTransport] Event payload (${serialized.length} bytes) exceeds limit (${this.maxPayloadBytes}). Consider breaking into smaller events or increasing maxPayloadBytes.`);
33
37
  const args = [
34
38
  this.stream,
35
39
  ...this.maxLen > 0 ? [
@@ -41,17 +45,20 @@ var RedisStreamTransport = class {
41
45
  "type",
42
46
  event.type,
43
47
  "data",
44
- JSON.stringify(event)
48
+ serialized
45
49
  ];
46
50
  await this.redis.xadd(...args);
47
51
  }
48
52
  async subscribe(pattern, handler) {
49
53
  if (!this.handlers.has(pattern)) this.handlers.set(pattern, /* @__PURE__ */ new Set());
50
- this.handlers.get(pattern).add(handler);
54
+ this.handlers.get(pattern)?.add(handler);
51
55
  if (!this.running) {
52
56
  await this.ensureGroup();
53
57
  this.running = true;
54
- this.pollPromise = this.pollLoop();
58
+ this.pollPromise = this.pollLoop().catch((err) => {
59
+ this.logger.error("[RedisStreamTransport] Poll loop crashed:", err);
60
+ this.running = false;
61
+ });
55
62
  }
56
63
  return () => {
57
64
  const set = this.handlers.get(pattern);
@@ -124,10 +131,16 @@ var RedisStreamTransport = class {
124
131
  }
125
132
  let event;
126
133
  try {
127
- event = JSON.parse(rawData, (key, value) => {
134
+ const parsed = JSON.parse(rawData, (key, value) => {
128
135
  if (key === "timestamp" && typeof value === "string") return new Date(value);
129
136
  return value;
130
137
  });
138
+ if (!parsed || typeof parsed !== "object" || typeof parsed.type !== "string" || !parsed.meta?.id) {
139
+ this.logger.warn("[RedisStreamTransport] Malformed event — missing type or meta.id, acking and skipping");
140
+ await this.redis.xack(this.stream, this.group, messageId);
141
+ return;
142
+ }
143
+ event = parsed;
131
144
  } catch {
132
145
  await this.redis.xack(this.stream, this.group, messageId);
133
146
  return;
@@ -152,7 +165,7 @@ var RedisStreamTransport = class {
152
165
  if (pattern === eventType) return true;
153
166
  if (pattern.endsWith(".*")) {
154
167
  const prefix = pattern.slice(0, -2);
155
- return eventType.startsWith(prefix + ".");
168
+ return eventType.startsWith(`${prefix}.`);
156
169
  }
157
170
  return false;
158
171
  }
@@ -172,6 +185,5 @@ var RedisStreamTransport = class {
172
185
  return new Promise((resolve) => setTimeout(resolve, ms));
173
186
  }
174
187
  };
175
-
176
188
  //#endregion
177
- export { RedisStreamTransport };
189
+ export { RedisStreamTransport };
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "../../EventTransport-BkUDYZEb.mjs";
1
+ import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "../../EventTransport-wc5hSLik.mjs";
2
2
 
3
3
  //#region src/events/transports/redis.d.ts
4
4
  interface RedisLike {