@classytic/arc 2.8.5 → 2.9.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 (140) hide show
  1. package/README.md +88 -5
  2. package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-Vu2yc56T.mjs} +188 -102
  3. package/dist/EventTransport-CqZ8FyM_.d.mts +293 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/audit/index.d.mts +100 -11
  6. package/dist/audit/index.mjs +71 -18
  7. package/dist/auth/index.d.mts +16 -8
  8. package/dist/auth/index.mjs +13 -6
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
  11. package/dist/cache/index.d.mts +2 -2
  12. package/dist/cache/index.mjs +2 -2
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/introspect.mjs +1 -1
  15. package/dist/core/index.d.mts +3 -3
  16. package/dist/core/index.mjs +4 -5
  17. package/dist/{core-F0QoWBt2.mjs → core-DNncu0xF.mjs} +1 -1
  18. package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
  19. package/dist/{createApp-B1EY8zxa.mjs → createApp-CBJUJKGP.mjs} +13 -12
  20. package/dist/{defineResource-tcgySDo1.mjs → defineResource-C__jkwvs.mjs} +22 -57
  21. package/dist/docs/index.d.mts +2 -2
  22. package/dist/docs/index.mjs +1 -1
  23. package/dist/dynamic/index.d.mts +1 -1
  24. package/dist/dynamic/index.mjs +3 -3
  25. package/dist/{elevation-DtFxrG0s.mjs → elevation-DxQ6ACbt.mjs} +21 -7
  26. package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
  27. package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
  28. package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
  29. package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
  30. package/dist/events/index.d.mts +147 -36
  31. package/dist/events/index.mjs +338 -101
  32. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  33. package/dist/events/transports/redis.d.mts +1 -1
  34. package/dist/factory/index.d.mts +1 -1
  35. package/dist/factory/index.mjs +2 -2
  36. package/dist/{fields-DpZQa_Q3.d.mts → fields-BC7zcmI9.d.mts} +15 -3
  37. package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
  38. package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-q8oHt--L.mjs} +65 -7
  39. package/dist/hooks/index.d.mts +1 -1
  40. package/dist/hooks/index.mjs +1 -1
  41. package/dist/idempotency/index.d.mts +29 -5
  42. package/dist/idempotency/index.mjs +111 -2
  43. package/dist/idempotency/redis.d.mts +1 -1
  44. package/dist/{index-BLXBmWud.d.mts → index-C-xjcA6F.d.mts} +1 -1
  45. package/dist/{index-DtDzOBn8.d.mts → index-Cibkchnx.d.mts} +3 -134
  46. package/dist/{index-C1meYuDn.d.mts → index-CtGKT0lf.d.mts} +1 -1
  47. package/dist/index.d.mts +7 -7
  48. package/dist/index.mjs +9 -9
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +26 -8
  53. package/dist/integrations/mcp/index.mjs +96 -17
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +5 -0
  57. package/dist/integrations/webhooks.mjs +6 -0
  58. package/dist/{interface-CMRutPfe.d.mts → interface-YrWsmKqE.d.mts} +287 -179
  59. package/dist/{openapi-CbKUJY_m.mjs → openapi-CXuTG1M9.mjs} +2 -2
  60. package/dist/org/index.d.mts +1 -1
  61. package/dist/permissions/index.d.mts +2 -2
  62. package/dist/permissions/index.mjs +3 -3
  63. package/dist/{permissions-CH4cNwJi.mjs → permissions-oNZawnkR.mjs} +1 -1
  64. package/dist/plugins/index.d.mts +7 -7
  65. package/dist/plugins/index.mjs +11 -11
  66. package/dist/plugins/response-cache.mjs +1 -1
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/policies/index.d.mts +25 -32
  70. package/dist/presets/filesUpload.d.mts +26 -4
  71. package/dist/presets/filesUpload.mjs +1 -1
  72. package/dist/presets/index.d.mts +3 -2
  73. package/dist/presets/index.mjs +4 -3
  74. package/dist/presets/multiTenant.d.mts +1 -1
  75. package/dist/presets/multiTenant.mjs +1 -1
  76. package/dist/presets/search.d.mts +91 -0
  77. package/dist/presets/search.mjs +150 -0
  78. package/dist/{presets-C2xgzW6x.mjs → presets-hM4WhNWY.mjs} +1 -1
  79. package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  80. package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
  81. package/dist/{redis-BM00zaPB.d.mts → redis-MXLp1oOf.d.mts} +1 -1
  82. package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
  83. package/dist/registry/index.d.mts +1 -1
  84. package/dist/registry/index.mjs +2 -2
  85. package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-C3cWymnW.mjs} +64 -47
  86. package/dist/rpc/index.d.mts +1 -1
  87. package/dist/rpc/index.mjs +1 -1
  88. package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
  89. package/dist/scope/index.mjs +1 -1
  90. package/dist/{sse-Ad7ypl9e.mjs → sse-CJpt7LGI.mjs} +1 -1
  91. package/dist/store-helpers-DFiZl5TL.mjs +57 -0
  92. package/dist/testing/index.d.mts +5 -14
  93. package/dist/testing/index.mjs +21 -75
  94. package/dist/testing/storageContract.d.mts +1 -1
  95. package/dist/types/index.d.mts +2 -2
  96. package/dist/types/storage.d.mts +1 -1
  97. package/dist/{types-BsbNMEDR.d.mts → types-CoSzA-s-.d.mts} +1 -1
  98. package/dist/{types-Ch9pTQbf.d.mts → types-CunEX4UX.d.mts} +10 -8
  99. package/dist/utils/index.d.mts +4 -4
  100. package/dist/utils/index.mjs +6 -6
  101. package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
  102. package/package.json +8 -11
  103. package/skills/arc/SKILL.md +92 -14
  104. package/skills/arc/references/auth.md +94 -0
  105. package/skills/arc/references/events.md +200 -12
  106. package/skills/arc/references/mcp.md +4 -17
  107. package/skills/arc/references/multi-tenancy.md +43 -0
  108. package/skills/arc/references/production.md +34 -19
  109. package/dist/EventTransport-BXja8NOc.d.mts +0 -135
  110. package/dist/audit/mongodb.d.mts +0 -2
  111. package/dist/audit/mongodb.mjs +0 -2
  112. package/dist/idempotency/mongodb.d.mts +0 -2
  113. package/dist/idempotency/mongodb.mjs +0 -123
  114. package/dist/mongodb-BsP-WbhN.d.mts +0 -127
  115. package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
  116. package/dist/mongodb-Utc5k_-0.mjs +0 -90
  117. /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  118. /package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-Dq3_zBQP.mjs} +0 -0
  119. /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
  120. /package/dist/{caching-IMuYVjTL.mjs → caching-CjybdRwx.mjs} +0 -0
  121. /package/dist/{circuitBreaker-dTtG-UyS.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  122. /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
  123. /package/dist/{errors-Ck2h67pm.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  124. /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
  125. /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  126. /package/dist/{interface-DfLGcus7.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  127. /package/dist/{interface-4y979v99.d.mts → interface-DplgQO2e.d.mts} +0 -0
  128. /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-Bksk8ydA.mjs} +0 -0
  129. /package/dist/{logger-D1YrIImS.mjs → logger-CDjpjySd.mjs} +0 -0
  130. /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
  131. /package/dist/{metrics-B-PU4-Yu.mjs → metrics-TuOmguhi.mjs} +0 -0
  132. /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
  133. /package/dist/{registry-BiTKT1Dg.mjs → registry-B0Wl7uVV.mjs} +0 -0
  134. /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  135. /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
  136. /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  137. /package/dist/{storage-Dfzt4VTl.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  138. /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  139. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
  140. /package/dist/{versioning-CDugduqI.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -1,4 +1,5 @@
1
- import { a as MemoryEventTransport, i as withRetry, o as createEvent, r as createDeadLetterPublisher, t as eventPlugin } from "../eventPlugin-CDjVTM82.mjs";
1
+ import { a as MemoryEventTransport, i as withRetry, o as createChildEvent, r as createDeadLetterPublisher, s as createEvent, t as eventPlugin } from "../eventPlugin-Dl7MoVWH.mjs";
2
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DFiZl5TL.mjs";
2
3
  //#region src/events/defineEvent.ts
3
4
  /**
4
5
  * defineEvent — Typed Event Definitions with Optional Schema Validation
@@ -223,6 +224,285 @@ const CACHE_EVENTS = Object.freeze({
223
224
  TAG_VERSION_BUMPED: "arc.cache.tag.bumped"
224
225
  });
225
226
  //#endregion
227
+ //#region src/events/memory-outbox.ts
228
+ const DEFAULT_LEASE_MS$2 = 3e4;
229
+ var MemoryOutboxStore = class {
230
+ entries = [];
231
+ seenDedupeKeys = /* @__PURE__ */ new Set();
232
+ async save(event, options) {
233
+ if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
234
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
235
+ if (options?.dedupeKey) {
236
+ if (this.seenDedupeKeys.has(options.dedupeKey)) return;
237
+ this.seenDedupeKeys.add(options.dedupeKey);
238
+ }
239
+ this.entries.push({
240
+ event,
241
+ status: "pending",
242
+ attempts: 0,
243
+ visibleAt: options?.visibleAt?.getTime() ?? 0,
244
+ leaseOwner: null,
245
+ leaseExpiresAt: 0,
246
+ deliveredAt: null,
247
+ firstFailedAt: null,
248
+ lastFailedAt: null,
249
+ lastError: null,
250
+ dedupeKey: options?.dedupeKey
251
+ });
252
+ }
253
+ async getPending(limit) {
254
+ const now = Date.now();
255
+ return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
256
+ }
257
+ async claimPending(options) {
258
+ const now = Date.now();
259
+ const limit = options?.limit ?? 100;
260
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$2;
261
+ const consumerId = options?.consumerId ?? "anonymous";
262
+ const typeFilter = options?.types ? new Set(options.types) : null;
263
+ const claimed = [];
264
+ for (const entry of this.entries) {
265
+ if (claimed.length >= limit) break;
266
+ if (entry.status !== "pending") continue;
267
+ if (entry.visibleAt > now) continue;
268
+ if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
269
+ if (typeFilter && !typeFilter.has(entry.event.type)) continue;
270
+ entry.leaseOwner = consumerId;
271
+ entry.leaseExpiresAt = now + leaseMs;
272
+ entry.attempts++;
273
+ claimed.push(entry.event);
274
+ }
275
+ return claimed;
276
+ }
277
+ async acknowledge(eventId, options) {
278
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
279
+ if (!entry) return;
280
+ if (entry.status === "delivered") return;
281
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
282
+ entry.status = "delivered";
283
+ entry.deliveredAt = Date.now();
284
+ entry.leaseOwner = null;
285
+ }
286
+ async fail(eventId, error, options) {
287
+ const entry = this.entries.find((e) => e.event.meta.id === eventId);
288
+ if (!entry) return;
289
+ if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
290
+ const now = Date.now();
291
+ entry.lastError = error;
292
+ entry.leaseOwner = null;
293
+ entry.leaseExpiresAt = 0;
294
+ if (entry.firstFailedAt === null) entry.firstFailedAt = now;
295
+ entry.lastFailedAt = now;
296
+ if (options?.deadLetter) {
297
+ entry.status = "dead_letter";
298
+ return;
299
+ }
300
+ entry.status = "pending";
301
+ entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
302
+ }
303
+ async getDeadLettered(limit) {
304
+ const out = [];
305
+ for (const entry of this.entries) {
306
+ if (out.length >= limit) break;
307
+ if (entry.status !== "dead_letter") continue;
308
+ out.push({
309
+ event: entry.event,
310
+ error: {
311
+ message: entry.lastError?.message ?? "unknown",
312
+ ...entry.lastError?.code !== void 0 ? { code: entry.lastError.code } : {}
313
+ },
314
+ attempts: entry.attempts,
315
+ firstFailedAt: new Date(entry.firstFailedAt ?? entry.lastFailedAt ?? Date.now()),
316
+ lastFailedAt: new Date(entry.lastFailedAt ?? entry.firstFailedAt ?? Date.now())
317
+ });
318
+ }
319
+ return out;
320
+ }
321
+ async purge(olderThanMs) {
322
+ const cutoff = Date.now() - olderThanMs;
323
+ let purged = 0;
324
+ for (let i = this.entries.length - 1; i >= 0; i--) {
325
+ const entry = this.entries[i];
326
+ if (!entry) continue;
327
+ if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
328
+ if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
329
+ this.entries.splice(i, 1);
330
+ purged++;
331
+ }
332
+ }
333
+ return purged;
334
+ }
335
+ /** Test helper: inspect entry by id */
336
+ _getEntry(eventId) {
337
+ return this.entries.find((e) => e.event.meta.id === eventId);
338
+ }
339
+ };
340
+ //#endregion
341
+ //#region src/events/repository-outbox-adapter.ts
342
+ const DEFAULT_LEASE_MS$1 = 3e4;
343
+ const DEFAULT_CLAIM_LIMIT = 100;
344
+ const DEFAULT_PURGE_BATCH = 500;
345
+ function repositoryAsOutboxStore(repository) {
346
+ const missing = [];
347
+ if (typeof repository.create !== "function") missing.push("create");
348
+ if (typeof repository.getOne !== "function") missing.push("getOne");
349
+ if (typeof repository.findAll !== "function") missing.push("findAll");
350
+ if (typeof repository.deleteMany !== "function") missing.push("deleteMany");
351
+ if (typeof repository.findOneAndUpdate !== "function") missing.push("findOneAndUpdate");
352
+ if (missing.length > 0) throw new Error(`EventOutbox: repository is missing required methods: ${missing.join(", ")}. mongokit ≥3.8 satisfies all five; other kits must implement them to back the outbox.`);
353
+ const r = repository;
354
+ const isDuplicateKeyError = createIsDuplicateKeyError(repository);
355
+ const safeGetOne = createSafeGetOne(repository);
356
+ const isWellFormed = (event) => !!event && typeof event.type === "string" && !!event.meta?.id;
357
+ return {
358
+ async save(event, options) {
359
+ if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
360
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
361
+ const now = /* @__PURE__ */ new Date();
362
+ const doc = {
363
+ _id: event.meta.id,
364
+ event,
365
+ type: event.type,
366
+ status: "pending",
367
+ attempts: 0,
368
+ visibleAt: options?.visibleAt ?? now,
369
+ leaseOwner: null,
370
+ leaseExpiresAt: null,
371
+ deliveredAt: null,
372
+ firstFailedAt: null,
373
+ lastFailedAt: null,
374
+ lastError: null,
375
+ dedupeKey: options?.dedupeKey ?? null,
376
+ partitionKey: options?.partitionKey ?? null,
377
+ headers: options?.headers ? { ...options.headers } : null,
378
+ createdAt: now
379
+ };
380
+ try {
381
+ await r.create(doc, options?.session ? { session: options.session } : void 0);
382
+ } catch (err) {
383
+ if (isDuplicateKeyError(err)) return;
384
+ throw err;
385
+ }
386
+ },
387
+ async getPending(limit) {
388
+ const now = /* @__PURE__ */ new Date();
389
+ return (await r.findAll({
390
+ status: "pending",
391
+ visibleAt: { $lte: now },
392
+ $or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }]
393
+ }, {
394
+ sort: { createdAt: 1 },
395
+ limit
396
+ })).map((d) => d.event).filter(isWellFormed);
397
+ },
398
+ async claimPending(options) {
399
+ const limit = options?.limit ?? DEFAULT_CLAIM_LIMIT;
400
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
401
+ const consumerId = options?.consumerId ?? "anonymous";
402
+ const typeFilter = options?.types?.length ? { type: { $in: options.types } } : {};
403
+ const claimed = [];
404
+ for (let i = 0; i < limit; i++) {
405
+ const now = /* @__PURE__ */ new Date();
406
+ const leaseExpiresAt = new Date(now.getTime() + leaseMs);
407
+ const doc = await r.findOneAndUpdate({
408
+ status: "pending",
409
+ visibleAt: { $lte: now },
410
+ $or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }],
411
+ ...typeFilter
412
+ }, {
413
+ $set: {
414
+ leaseOwner: consumerId,
415
+ leaseExpiresAt
416
+ },
417
+ $inc: { attempts: 1 }
418
+ }, {
419
+ sort: { createdAt: 1 },
420
+ returnDocument: "after"
421
+ });
422
+ if (!doc) break;
423
+ if (isWellFormed(doc.event)) claimed.push(doc.event);
424
+ }
425
+ return claimed;
426
+ },
427
+ async acknowledge(eventId, options) {
428
+ const now = /* @__PURE__ */ new Date();
429
+ const filter = {
430
+ _id: eventId,
431
+ status: { $ne: "delivered" }
432
+ };
433
+ if (options?.consumerId) filter.leaseOwner = options.consumerId;
434
+ if (await r.findOneAndUpdate(filter, { $set: {
435
+ status: "delivered",
436
+ deliveredAt: now,
437
+ leaseOwner: null,
438
+ leaseExpiresAt: null
439
+ } }, { returnDocument: "after" })) return;
440
+ const current = await safeGetOne({ _id: eventId });
441
+ if (!current) return;
442
+ if (current.status === "delivered") return;
443
+ if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
444
+ },
445
+ async fail(eventId, error, options) {
446
+ const now = /* @__PURE__ */ new Date();
447
+ const targetStatus = options?.deadLetter ? "dead_letter" : "pending";
448
+ const visibleAt = options?.retryAt ?? now;
449
+ const filter = { _id: eventId };
450
+ if (options?.consumerId) filter.leaseOwner = options.consumerId;
451
+ const pipeline = [{ $set: {
452
+ status: targetStatus,
453
+ visibleAt,
454
+ leaseOwner: null,
455
+ leaseExpiresAt: null,
456
+ lastFailedAt: now,
457
+ lastError: {
458
+ message: error.message,
459
+ ...error.code ? { code: error.code } : {}
460
+ },
461
+ firstFailedAt: { $ifNull: ["$firstFailedAt", now] }
462
+ } }];
463
+ if (await r.findOneAndUpdate(filter, pipeline, { returnDocument: "after" })) return;
464
+ const current = await safeGetOne({ _id: eventId });
465
+ if (!current) return;
466
+ if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
467
+ },
468
+ async getDeadLettered(limit) {
469
+ return (await r.findAll({ status: "dead_letter" }, {
470
+ sort: { _id: 1 },
471
+ limit
472
+ })).filter((d) => isWellFormed(d.event)).map((d) => ({
473
+ event: d.event,
474
+ error: {
475
+ message: d.lastError?.message ?? "unknown",
476
+ ...d.lastError?.code !== void 0 ? { code: d.lastError.code } : {}
477
+ },
478
+ attempts: d.attempts,
479
+ firstFailedAt: d.firstFailedAt ?? d.lastFailedAt ?? d.createdAt,
480
+ lastFailedAt: d.lastFailedAt ?? d.firstFailedAt ?? d.createdAt
481
+ }));
482
+ },
483
+ async purge(olderThanMs) {
484
+ const cutoff = new Date(Date.now() - olderThanMs);
485
+ let totalDeleted = 0;
486
+ for (;;) {
487
+ const batch = await r.findAll({
488
+ status: "delivered",
489
+ deliveredAt: { $lte: cutoff }
490
+ }, {
491
+ sort: { deliveredAt: 1 },
492
+ limit: DEFAULT_PURGE_BATCH,
493
+ select: "_id"
494
+ });
495
+ if (batch.length === 0) break;
496
+ const ids = batch.map((d) => d._id);
497
+ const res = await r.deleteMany({ _id: { $in: ids } });
498
+ totalDeleted += res.deletedCount ?? 0;
499
+ if (batch.length < DEFAULT_PURGE_BATCH) break;
500
+ }
501
+ return totalDeleted;
502
+ }
503
+ };
504
+ }
505
+ //#endregion
226
506
  //#region src/events/outbox.ts
227
507
  /** Default outbox retention — delivered events older than this are eligible for purge */
228
508
  const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
@@ -263,14 +543,25 @@ var EventOutbox = class {
263
543
  _leaseMs;
264
544
  _onError;
265
545
  _usePublishMany;
546
+ _failurePolicy;
547
+ /**
548
+ * In-process attempt counter per event id. Accurate within this relay
549
+ * process; resets on restart. Populated as failures occur and cleared on
550
+ * successful ack or dead-letter transition. For durable authoritative
551
+ * counts, apps can query the store directly inside {@link OutboxFailurePolicy}.
552
+ */
553
+ _attempts = /* @__PURE__ */ new Map();
266
554
  constructor(opts) {
267
- this._store = opts.store;
555
+ if (opts.repository) this._store = repositoryAsOutboxStore(opts.repository);
556
+ else if (opts.store) this._store = opts.store;
557
+ else throw new Error("EventOutbox: either `repository` or `store` must be provided. Pass a RepositoryLike (mongokit / prismakit) for the common case, or a concrete OutboxStore (memory / custom) for non-repository backends.");
268
558
  this._transport = opts.transport;
269
559
  this._batchSize = opts.batchSize ?? 100;
270
560
  this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
271
561
  this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
272
562
  this._onError = opts.onError;
273
563
  this._usePublishMany = opts.usePublishMany ?? true;
564
+ this._failurePolicy = opts.failurePolicy;
274
565
  }
275
566
  /** Unique consumer ID used for lease ownership when the store supports claims */
276
567
  get consumerId() {
@@ -292,7 +583,11 @@ var EventOutbox = class {
292
583
  if (!event || typeof event !== "object") throw new InvalidOutboxEventError("event is not an object");
293
584
  if (!event.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
294
585
  if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
295
- await this._store.save(event, options);
586
+ const effectiveOptions = options?.dedupeKey === void 0 && event.meta.idempotencyKey ? {
587
+ ...options ?? {},
588
+ dedupeKey: event.meta.idempotencyKey
589
+ } : options;
590
+ await this._store.save(event, effectiveOptions);
296
591
  }
297
592
  _reportError(kind, error, event) {
298
593
  if (!this._onError) return;
@@ -357,6 +652,7 @@ var EventOutbox = class {
357
652
  ownershipMismatches: 0,
358
653
  malformed: 0,
359
654
  failHookErrors: 0,
655
+ deadLettered: 0,
360
656
  usedPublishMany: false
361
657
  };
362
658
  if (!this._transport) return empty;
@@ -380,7 +676,8 @@ var EventOutbox = class {
380
676
  publishFailed: 0,
381
677
  ackFailed: 0,
382
678
  ownershipMismatches: 0,
383
- failHookErrors: 0
679
+ failHookErrors: 0,
680
+ deadLettered: 0
384
681
  };
385
682
  const canPublishMany = this._usePublishMany && typeof this._transport.publishMany === "function";
386
683
  const canFail = typeof this._store.fail === "function";
@@ -414,8 +711,28 @@ var EventOutbox = class {
414
711
  stopBatch = true;
415
712
  continue;
416
713
  }
714
+ const attempts = (this._attempts.get(event.meta.id) ?? 0) + 1;
715
+ this._attempts.set(event.meta.id, attempts);
716
+ let failOpts = { consumerId: this._consumerId };
717
+ if (this._failurePolicy) try {
718
+ const decision = await this._failurePolicy({
719
+ event,
720
+ error: publishErr,
721
+ attempts
722
+ });
723
+ failOpts = {
724
+ ...failOpts,
725
+ ...decision
726
+ };
727
+ } catch (policyErr) {
728
+ this._reportError("fail_failed", policyErr, event);
729
+ }
417
730
  try {
418
- await this._store.fail(event.meta.id, normalizeError(publishErr), { consumerId: this._consumerId });
731
+ await this._store.fail(event.meta.id, normalizeError(publishErr), failOpts);
732
+ if (failOpts.deadLetter) {
733
+ counts.deadLettered++;
734
+ this._attempts.delete(event.meta.id);
735
+ }
419
736
  } catch (failErr) {
420
737
  if (failErr instanceof OutboxOwnershipError) {
421
738
  counts.ownershipMismatches++;
@@ -430,6 +747,7 @@ var EventOutbox = class {
430
747
  try {
431
748
  await this._store.acknowledge(event.meta.id, { consumerId: this._consumerId });
432
749
  counts.relayed++;
750
+ this._attempts.delete(event.meta.id);
433
751
  } catch (ackErr) {
434
752
  counts.ackFailed++;
435
753
  if (ackErr instanceof OutboxOwnershipError) {
@@ -446,10 +764,24 @@ var EventOutbox = class {
446
764
  ownershipMismatches: counts.ownershipMismatches,
447
765
  malformed,
448
766
  failHookErrors: counts.failHookErrors,
767
+ deadLettered: counts.deadLettered,
449
768
  usedPublishMany: canPublishMany && valid.length > 0
450
769
  };
451
770
  }
452
771
  /**
772
+ * Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
773
+ * envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
774
+ * `[]` when the store doesn't implement it.
775
+ *
776
+ * Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
777
+ * `deadLetter: true` after N attempts, then read back with this to alert,
778
+ * replay, or archive.
779
+ */
780
+ async getDeadLettered(limit = 100) {
781
+ if (!this._store.getDeadLettered) return [];
782
+ return this._store.getDeadLettered(limit);
783
+ }
784
+ /**
453
785
  * Purge old **delivered** events from the outbox store.
454
786
  * Delegates to `store.purge()` if implemented; no-op otherwise.
455
787
  * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
@@ -468,101 +800,6 @@ function normalizeError(err) {
468
800
  return { message: String(err) };
469
801
  }
470
802
  /**
471
- * In-memory outbox store — reference implementation supporting the full
472
- * capability set (claim/lease, fail/retry, dedupe, visibleAt).
473
- *
474
- * For dev/testing only. Production deployments should use a durable store
475
- * backed by the app's database.
476
- */
477
- var MemoryOutboxStore = class {
478
- entries = [];
479
- seenDedupeKeys = /* @__PURE__ */ new Set();
480
- async save(event, options) {
481
- if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
482
- if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
483
- if (options?.dedupeKey) {
484
- if (this.seenDedupeKeys.has(options.dedupeKey)) return;
485
- this.seenDedupeKeys.add(options.dedupeKey);
486
- }
487
- this.entries.push({
488
- event,
489
- status: "pending",
490
- attempts: 0,
491
- visibleAt: options?.visibleAt?.getTime() ?? 0,
492
- leaseOwner: null,
493
- leaseExpiresAt: 0,
494
- deliveredAt: null,
495
- lastError: null,
496
- dedupeKey: options?.dedupeKey
497
- });
498
- }
499
- async getPending(limit) {
500
- const now = Date.now();
501
- return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
502
- }
503
- async claimPending(options) {
504
- const now = Date.now();
505
- const limit = options?.limit ?? 100;
506
- const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
507
- const consumerId = options?.consumerId ?? "anonymous";
508
- const typeFilter = options?.types ? new Set(options.types) : null;
509
- const claimed = [];
510
- for (const entry of this.entries) {
511
- if (claimed.length >= limit) break;
512
- if (entry.status !== "pending") continue;
513
- if (entry.visibleAt > now) continue;
514
- if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
515
- if (typeFilter && !typeFilter.has(entry.event.type)) continue;
516
- entry.leaseOwner = consumerId;
517
- entry.leaseExpiresAt = now + leaseMs;
518
- entry.attempts++;
519
- claimed.push(entry.event);
520
- }
521
- return claimed;
522
- }
523
- async acknowledge(eventId, options) {
524
- const entry = this.entries.find((e) => e.event.meta.id === eventId);
525
- if (!entry) return;
526
- if (entry.status === "delivered") return;
527
- if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
528
- entry.status = "delivered";
529
- entry.deliveredAt = Date.now();
530
- entry.leaseOwner = null;
531
- }
532
- async fail(eventId, error, options) {
533
- const entry = this.entries.find((e) => e.event.meta.id === eventId);
534
- if (!entry) return;
535
- if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
536
- entry.lastError = error;
537
- entry.leaseOwner = null;
538
- entry.leaseExpiresAt = 0;
539
- if (options?.deadLetter) {
540
- entry.status = "dead_letter";
541
- return;
542
- }
543
- entry.status = "pending";
544
- entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
545
- }
546
- async purge(olderThanMs) {
547
- const cutoff = Date.now() - olderThanMs;
548
- let purged = 0;
549
- for (let i = this.entries.length - 1; i >= 0; i--) {
550
- const entry = this.entries[i];
551
- if (!entry) continue;
552
- if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
553
- if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
554
- this.entries.splice(i, 1);
555
- purged++;
556
- }
557
- }
558
- return purged;
559
- }
560
- /** Test helper: inspect entry by id */
561
- _getEntry(eventId) {
562
- return this.entries.find((e) => e.event.meta.id === eventId);
563
- }
564
- };
565
- /**
566
803
  * Compute a `retryAt` `Date` using exponential backoff with jitter.
567
804
  *
568
805
  * This is a convenience helper for store authors implementing
@@ -606,4 +843,4 @@ function exponentialBackoff(options) {
606
843
  return new Date(now + jittered);
607
844
  }
608
845
  //#endregion
609
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
846
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createChildEvent, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
@@ -1,2 +1,2 @@
1
- import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-CrsfUmPt.mjs";
1
+ import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-Bz-4q96t.mjs";
2
2
  export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
@@ -1,4 +1,4 @@
1
- import { i as EventTransport, n as EventHandler, r as EventLogger, t as DomainEvent } from "../../EventTransport-BXja8NOc.mjs";
1
+ import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "../../EventTransport-CqZ8FyM_.mjs";
2
2
 
3
3
  //#region src/events/transports/redis.d.ts
4
4
  interface RedisLike {
@@ -1,4 +1,4 @@
1
- import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-Ch9pTQbf.mjs";
1
+ import { a as CustomPluginAuthOption, c as RawBodyOptions, d as ResourceLike, f as loadResources, i as CustomAuthenticatorOption, l as UnderPressureOptions, n as BetterAuthOption, o as JwtAuthOption, r as CreateAppOptions, s as MultipartOptions, t as AuthOption, u as LoadResourcesOptions } from "../types-CunEX4UX.mjs";
2
2
  import { FastifyInstance } from "fastify";
3
3
 
4
4
  //#region src/factory/createApp.d.ts
@@ -1,5 +1,5 @@
1
- import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-B1EY8zxa.mjs";
2
- import { t as loadResources } from "../loadResources-PWd0OCpV.mjs";
1
+ import { a as edgePreset, c as testingPreset, i as developmentPreset, n as createApp, o as getPreset, s as productionPreset, t as ArcFactory } from "../createApp-CBJUJKGP.mjs";
2
+ import { t as loadResources } from "../loadResources-Bksk8ydA.mjs";
3
3
  //#region src/factory/edge.ts
4
4
  /**
5
5
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -86,16 +86,28 @@ declare const fields: {
86
86
  * @returns The filtered object
87
87
  */
88
88
  declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
89
+ /**
90
+ * Result of applying write permissions — includes both the filtered body
91
+ * and the list of fields that were stripped so callers can decide whether
92
+ * to reject the request (secure default) or silently strip (legacy).
93
+ */
94
+ interface FieldWritePermissionResult<T extends Record<string, unknown>> {
95
+ readonly body: T;
96
+ readonly deniedFields: readonly string[];
97
+ }
89
98
  /**
90
99
  * Apply field-level WRITE permissions to request body.
91
- * Strips fields that the user doesn't have permission to write.
100
+ *
101
+ * Returns both the filtered body and the list of denied fields. Callers are
102
+ * expected to reject the request when `deniedFields.length > 0` — silently
103
+ * stripping fields hides misconfigurations and real attacks. See
104
+ * `BodySanitizer` for the default policy.
92
105
  *
93
106
  * @param body - The request body (returns a new filtered copy)
94
107
  * @param fieldPermissions - Field permission map from resource config
95
108
  * @param userRoles - Current user's roles
96
- * @returns Filtered body
97
109
  */
98
- declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
110
+ declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
99
111
  /**
100
112
  * Resolve effective roles by merging global user roles with org-level roles.
101
113
  *
@@ -77,24 +77,37 @@ function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
77
77
  }
78
78
  /**
79
79
  * Apply field-level WRITE permissions to request body.
80
- * Strips fields that the user doesn't have permission to write.
80
+ *
81
+ * Returns both the filtered body and the list of denied fields. Callers are
82
+ * expected to reject the request when `deniedFields.length > 0` — silently
83
+ * stripping fields hides misconfigurations and real attacks. See
84
+ * `BodySanitizer` for the default policy.
81
85
  *
82
86
  * @param body - The request body (returns a new filtered copy)
83
87
  * @param fieldPermissions - Field permission map from resource config
84
88
  * @param userRoles - Current user's roles
85
- * @returns Filtered body
86
89
  */
87
90
  function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
88
91
  const result = { ...body };
92
+ const deniedFields = [];
89
93
  for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
90
94
  case "hidden":
91
- delete result[field];
95
+ if (field in result) {
96
+ deniedFields.push(field);
97
+ delete result[field];
98
+ }
92
99
  break;
93
100
  case "writableBy":
94
- if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) delete result[field];
101
+ if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
102
+ deniedFields.push(field);
103
+ delete result[field];
104
+ }
95
105
  break;
96
106
  }
97
- return result;
107
+ return {
108
+ body: result,
109
+ deniedFields
110
+ };
98
111
  }
99
112
  /**
100
113
  * Resolve effective roles by merging global user roles with org-level roles.