@classytic/arc 2.8.5 → 2.10.3

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 (155) hide show
  1. package/README.md +50 -38
  2. package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-CbKKIflT.mjs} +193 -143
  3. package/dist/EventTransport-CUw5NNWe.d.mts +293 -0
  4. package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
  5. package/dist/adapters/index.d.mts +3 -3
  6. package/dist/adapters/index.mjs +2 -2
  7. package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
  8. package/dist/audit/index.d.mts +135 -11
  9. package/dist/audit/index.mjs +107 -20
  10. package/dist/auth/index.d.mts +17 -9
  11. package/dist/auth/index.mjs +14 -7
  12. package/dist/auth/redis-session.d.mts +1 -1
  13. package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +1 -1
  14. package/dist/cache/index.d.mts +17 -15
  15. package/dist/cache/index.mjs +15 -14
  16. package/dist/{caching-IMuYVjTL.mjs → caching-CBpK_SCM.mjs} +8 -3
  17. package/dist/cli/commands/describe.mjs +1 -1
  18. package/dist/cli/commands/docs.mjs +2 -2
  19. package/dist/cli/commands/generate.mjs +1 -1
  20. package/dist/cli/commands/init.mjs +1 -1
  21. package/dist/cli/commands/introspect.mjs +1 -1
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +4 -6
  24. package/dist/{defineResource-tcgySDo1.mjs → core-CcR01lup.mjs} +58 -61
  25. package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-Bp_5c_2b.mjs} +3 -3
  26. package/dist/{createApp-B1EY8zxa.mjs → createApp-BuvPma24.mjs} +15 -14
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +2 -2
  29. package/dist/{elevation-DtFxrG0s.mjs → elevation-C7hgL_aI.mjs} +22 -8
  30. package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-Bb49BvPD.mjs} +59 -7
  31. package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DRQ3EqfL.d.mts} +37 -2
  32. package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-CxWgpd6K.d.mts} +14 -2
  33. package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-DCUjuiQT.mjs} +83 -5
  34. package/dist/events/index.d.mts +150 -36
  35. package/dist/events/index.mjs +355 -101
  36. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  37. package/dist/events/transports/redis.d.mts +1 -1
  38. package/dist/factory/index.d.mts +1 -1
  39. package/dist/factory/index.mjs +2 -2
  40. package/dist/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
  41. package/dist/{fields-ipsbIRPK.mjs → fields-bxkeltzz.mjs} +18 -5
  42. package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-t21LS-py.mjs} +65 -7
  43. package/dist/hooks/index.d.mts +1 -1
  44. package/dist/hooks/index.mjs +1 -1
  45. package/dist/idempotency/index.d.mts +32 -5
  46. package/dist/idempotency/index.mjs +119 -12
  47. package/dist/idempotency/redis.d.mts +1 -1
  48. package/dist/{index-DtDzOBn8.d.mts → index-8qw4y6ff.d.mts} +4 -135
  49. package/dist/{index-BLXBmWud.d.mts → index-ChIw3776.d.mts} +283 -408
  50. package/dist/{interface-CMRutPfe.d.mts → index-Cl0uoKd5.d.mts} +1758 -2506
  51. package/dist/{index-C1meYuDn.d.mts → index-DStwgFUK.d.mts} +81 -7
  52. package/dist/index.d.mts +7 -8
  53. package/dist/index.mjs +11 -12
  54. package/dist/integrations/event-gateway.d.mts +1 -1
  55. package/dist/integrations/event-gateway.mjs +1 -1
  56. package/dist/integrations/index.d.mts +1 -1
  57. package/dist/integrations/mcp/index.d.mts +26 -8
  58. package/dist/integrations/mcp/index.mjs +96 -17
  59. package/dist/integrations/mcp/testing.d.mts +1 -1
  60. package/dist/integrations/mcp/testing.mjs +1 -1
  61. package/dist/integrations/webhooks.d.mts +5 -0
  62. package/dist/integrations/webhooks.mjs +6 -0
  63. package/dist/interface-D218ikEo.d.mts +77 -0
  64. package/dist/{memory-Cp7_cAko.mjs → memory-B5Amv9A1.mjs} +23 -8
  65. package/dist/{openapi-CbKUJY_m.mjs → openapi-B5F8AddX.mjs} +3 -3
  66. package/dist/org/index.d.mts +2 -2
  67. package/dist/permissions/index.d.mts +3 -4
  68. package/dist/permissions/index.mjs +5 -5
  69. package/dist/{permissions-CH4cNwJi.mjs → permissions-Dk6mshja.mjs} +315 -397
  70. package/dist/plugins/index.d.mts +7 -7
  71. package/dist/plugins/index.mjs +14 -16
  72. package/dist/plugins/response-cache.mjs +2 -2
  73. package/dist/plugins/tracing-entry.d.mts +1 -1
  74. package/dist/plugins/tracing-entry.mjs +1 -1
  75. package/dist/presets/filesUpload.d.mts +27 -5
  76. package/dist/presets/filesUpload.mjs +1 -1
  77. package/dist/presets/index.d.mts +3 -2
  78. package/dist/presets/index.mjs +4 -3
  79. package/dist/presets/multiTenant.d.mts +1 -1
  80. package/dist/presets/multiTenant.mjs +2 -2
  81. package/dist/presets/search.d.mts +178 -0
  82. package/dist/presets/search.mjs +150 -0
  83. package/dist/{presets-C2xgzW6x.mjs → presets-fLJVXdVn.mjs} +1 -1
  84. package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
  85. package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DQCEfJis.mjs} +9 -9
  86. package/dist/{queryParser-CgCtsjti.mjs → queryParser-DBqBB6AC.mjs} +1 -1
  87. package/dist/{redis-BM00zaPB.d.mts → redis-DqyeggCa.d.mts} +1 -1
  88. package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-CakIQmwR.d.mts} +1 -1
  89. package/dist/registry/index.d.mts +1 -1
  90. package/dist/registry/index.mjs +2 -2
  91. package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-BElv3xPT.mjs} +65 -48
  92. package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
  93. package/dist/scope/index.d.mts +1 -1
  94. package/dist/scope/index.mjs +2 -2
  95. package/dist/{sse-Ad7ypl9e.mjs → sse-yBCgOLGu.mjs} +1 -1
  96. package/dist/store-helpers-ZCSMJJAX.mjs +57 -0
  97. package/dist/testing/index.d.mts +9 -17
  98. package/dist/testing/index.mjs +27 -83
  99. package/dist/testing/storageContract.d.mts +1 -1
  100. package/dist/types/index.d.mts +4 -4
  101. package/dist/types/index.mjs +1 -31
  102. package/dist/types/storage.d.mts +1 -1
  103. package/dist/{types-BsbNMEDR.d.mts → types-Btdda02s.d.mts} +1 -1
  104. package/dist/{types-Ch9pTQbf.d.mts → types-Co8k3NyS.d.mts} +11 -9
  105. package/dist/types-Csi3FLfq.mjs +27 -0
  106. package/dist/utils/index.d.mts +208 -4
  107. package/dist/utils/index.mjs +5 -6
  108. package/dist/{utils-yYT3HDXt.mjs → utils-B2fNOD_i.mjs} +285 -2
  109. package/dist/{versioning-CDugduqI.mjs → versioning-C2U_bLY0.mjs} +3 -5
  110. package/package.json +20 -26
  111. package/skills/arc/SKILL.md +97 -23
  112. package/skills/arc/references/auth.md +94 -0
  113. package/skills/arc/references/events.md +200 -12
  114. package/skills/arc/references/mcp.md +4 -17
  115. package/skills/arc/references/multi-tenancy.md +43 -0
  116. package/skills/arc/references/production.md +34 -60
  117. package/dist/EventTransport-BXja8NOc.d.mts +0 -135
  118. package/dist/audit/mongodb.d.mts +0 -2
  119. package/dist/audit/mongodb.mjs +0 -2
  120. package/dist/circuitBreaker-cmi5XDv5.mjs +0 -284
  121. package/dist/circuitBreaker-dTtG-UyS.d.mts +0 -206
  122. package/dist/core-F0QoWBt2.mjs +0 -34
  123. package/dist/dynamic/index.d.mts +0 -93
  124. package/dist/dynamic/index.mjs +0 -122
  125. package/dist/fields-DpZQa_Q3.d.mts +0 -109
  126. package/dist/idempotency/mongodb.d.mts +0 -2
  127. package/dist/idempotency/mongodb.mjs +0 -123
  128. package/dist/interface-4y979v99.d.mts +0 -54
  129. package/dist/mongodb-BsP-WbhN.d.mts +0 -127
  130. package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
  131. package/dist/mongodb-Utc5k_-0.mjs +0 -90
  132. package/dist/policies/index.d.mts +0 -432
  133. package/dist/policies/index.mjs +0 -318
  134. package/dist/rpc/index.d.mts +0 -90
  135. package/dist/rpc/index.mjs +0 -248
  136. /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
  137. /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
  138. /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
  139. /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
  140. /package/dist/{errors-Ck2h67pm.d.mts → errors-CCSsMpXE.d.mts} +0 -0
  141. /package/dist/{errors-BF2bIOIS.mjs → errors-D5c-5BJL.mjs} +0 -0
  142. /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
  143. /package/dist/{interface-DfLGcus7.d.mts → interface-CSbZdv_3.d.mts} +0 -0
  144. /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-BAzJItAJ.mjs} +0 -0
  145. /package/dist/{logger-D1YrIImS.mjs → logger-DLg8-Ueg.mjs} +0 -0
  146. /package/dist/{metrics-B-PU4-Yu.mjs → metrics-DuhiSEZI.mjs} +0 -0
  147. /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
  148. /package/dist/{registry-BiTKT1Dg.mjs → registry-B3lRFBWo.mjs} +0 -0
  149. /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
  150. /package/dist/{requestContext-DYvHl113.mjs → requestContext-xHIKedG6.mjs} +0 -0
  151. /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
  152. /package/dist/{storage-Dfzt4VTl.d.mts → storage-CVk_SEn2.d.mts} +0 -0
  153. /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-65B51Dw3.d.mts} +0 -0
  154. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
  155. /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.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-DCUjuiQT.mjs";
2
+ import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-ZCSMJJAX.mjs";
2
3
  //#region src/events/defineEvent.ts
3
4
  /**
4
5
  * defineEvent — Typed Event Definitions with Optional Schema Validation
@@ -223,6 +224,302 @@ 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.getAll !== "function") missing.push("getAll");
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.10.2 satisfies all five; other kits must implement them to back the outbox.`);
353
+ const r = repository;
354
+ /**
355
+ * Unwrap mongokit's pagination envelope ({ docs, total, ... }) — some
356
+ * kits may return a bare array when pagination is disabled. Handle both.
357
+ */
358
+ const unwrapDocs = (result) => {
359
+ if (Array.isArray(result)) return result;
360
+ return result?.docs ?? [];
361
+ };
362
+ const isDuplicateKeyError = createIsDuplicateKeyError(repository);
363
+ const safeGetOne = createSafeGetOne(repository);
364
+ const isWellFormed = (event) => !!event && typeof event.type === "string" && !!event.meta?.id;
365
+ return {
366
+ async save(event, options) {
367
+ if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
368
+ if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
369
+ const now = /* @__PURE__ */ new Date();
370
+ const doc = {
371
+ _id: event.meta.id,
372
+ event,
373
+ type: event.type,
374
+ status: "pending",
375
+ attempts: 0,
376
+ visibleAt: options?.visibleAt ?? now,
377
+ leaseOwner: null,
378
+ leaseExpiresAt: null,
379
+ deliveredAt: null,
380
+ firstFailedAt: null,
381
+ lastFailedAt: null,
382
+ lastError: null,
383
+ dedupeKey: options?.dedupeKey ?? null,
384
+ partitionKey: options?.partitionKey ?? null,
385
+ headers: options?.headers ? { ...options.headers } : null,
386
+ createdAt: now
387
+ };
388
+ try {
389
+ await r.create(doc, options?.session ? { session: options.session } : void 0);
390
+ } catch (err) {
391
+ if (isDuplicateKeyError(err)) return;
392
+ throw err;
393
+ }
394
+ },
395
+ async getPending(limit) {
396
+ const now = /* @__PURE__ */ new Date();
397
+ return unwrapDocs(await r.getAll({
398
+ filters: {
399
+ status: "pending",
400
+ visibleAt: { $lte: now },
401
+ $or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }]
402
+ },
403
+ sort: { createdAt: 1 },
404
+ page: 1,
405
+ limit
406
+ })).map((d) => d.event).filter(isWellFormed);
407
+ },
408
+ async claimPending(options) {
409
+ const limit = options?.limit ?? DEFAULT_CLAIM_LIMIT;
410
+ const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
411
+ const consumerId = options?.consumerId ?? "anonymous";
412
+ const typeFilter = options?.types?.length ? { type: { $in: options.types } } : {};
413
+ const claimed = [];
414
+ for (let i = 0; i < limit; i++) {
415
+ const now = /* @__PURE__ */ new Date();
416
+ const leaseExpiresAt = new Date(now.getTime() + leaseMs);
417
+ const doc = await r.findOneAndUpdate({
418
+ status: "pending",
419
+ visibleAt: { $lte: now },
420
+ $or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }],
421
+ ...typeFilter
422
+ }, {
423
+ $set: {
424
+ leaseOwner: consumerId,
425
+ leaseExpiresAt
426
+ },
427
+ $inc: { attempts: 1 }
428
+ }, {
429
+ sort: { createdAt: 1 },
430
+ returnDocument: "after"
431
+ });
432
+ if (!doc) break;
433
+ if (isWellFormed(doc.event)) claimed.push(doc.event);
434
+ }
435
+ return claimed;
436
+ },
437
+ async acknowledge(eventId, options) {
438
+ const now = /* @__PURE__ */ new Date();
439
+ const filter = {
440
+ _id: eventId,
441
+ status: { $ne: "delivered" }
442
+ };
443
+ if (options?.consumerId) filter.leaseOwner = options.consumerId;
444
+ if (await r.findOneAndUpdate(filter, { $set: {
445
+ status: "delivered",
446
+ deliveredAt: now,
447
+ leaseOwner: null,
448
+ leaseExpiresAt: null
449
+ } }, { returnDocument: "after" })) return;
450
+ const current = await safeGetOne({ _id: eventId });
451
+ if (!current) return;
452
+ if (current.status === "delivered") return;
453
+ if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
454
+ },
455
+ async fail(eventId, error, options) {
456
+ const now = /* @__PURE__ */ new Date();
457
+ const targetStatus = options?.deadLetter ? "dead_letter" : "pending";
458
+ const visibleAt = options?.retryAt ?? now;
459
+ const filter = { _id: eventId };
460
+ if (options?.consumerId) filter.leaseOwner = options.consumerId;
461
+ const pipeline = [{ $set: {
462
+ status: targetStatus,
463
+ visibleAt,
464
+ leaseOwner: null,
465
+ leaseExpiresAt: null,
466
+ lastFailedAt: now,
467
+ lastError: {
468
+ message: error.message,
469
+ ...error.code ? { code: error.code } : {}
470
+ },
471
+ firstFailedAt: { $ifNull: ["$firstFailedAt", now] }
472
+ } }];
473
+ if (await r.findOneAndUpdate(filter, pipeline, {
474
+ returnDocument: "after",
475
+ updatePipeline: true
476
+ })) return;
477
+ const current = await safeGetOne({ _id: eventId });
478
+ if (!current) return;
479
+ if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
480
+ },
481
+ async getDeadLettered(limit) {
482
+ return unwrapDocs(await r.getAll({
483
+ filters: { status: "dead_letter" },
484
+ sort: { _id: 1 },
485
+ page: 1,
486
+ limit
487
+ })).filter((d) => isWellFormed(d.event)).map((d) => ({
488
+ event: d.event,
489
+ error: {
490
+ message: d.lastError?.message ?? "unknown",
491
+ ...d.lastError?.code !== void 0 ? { code: d.lastError.code } : {}
492
+ },
493
+ attempts: d.attempts,
494
+ firstFailedAt: d.firstFailedAt ?? d.lastFailedAt ?? d.createdAt,
495
+ lastFailedAt: d.lastFailedAt ?? d.firstFailedAt ?? d.createdAt
496
+ }));
497
+ },
498
+ async purge(olderThanMs) {
499
+ const cutoff = new Date(Date.now() - olderThanMs);
500
+ let totalDeleted = 0;
501
+ for (;;) {
502
+ const batch = unwrapDocs(await r.getAll({
503
+ filters: {
504
+ status: "delivered",
505
+ deliveredAt: { $lte: cutoff }
506
+ },
507
+ sort: { deliveredAt: 1 },
508
+ page: 1,
509
+ limit: DEFAULT_PURGE_BATCH,
510
+ select: "_id"
511
+ }));
512
+ if (batch.length === 0) break;
513
+ const ids = batch.map((d) => d._id);
514
+ const res = await r.deleteMany({ _id: { $in: ids } });
515
+ totalDeleted += res.deletedCount ?? 0;
516
+ if (batch.length < DEFAULT_PURGE_BATCH) break;
517
+ }
518
+ return totalDeleted;
519
+ }
520
+ };
521
+ }
522
+ //#endregion
226
523
  //#region src/events/outbox.ts
227
524
  /** Default outbox retention — delivered events older than this are eligible for purge */
228
525
  const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
@@ -263,14 +560,25 @@ var EventOutbox = class {
263
560
  _leaseMs;
264
561
  _onError;
265
562
  _usePublishMany;
563
+ _failurePolicy;
564
+ /**
565
+ * In-process attempt counter per event id. Accurate within this relay
566
+ * process; resets on restart. Populated as failures occur and cleared on
567
+ * successful ack or dead-letter transition. For durable authoritative
568
+ * counts, apps can query the store directly inside {@link OutboxFailurePolicy}.
569
+ */
570
+ _attempts = /* @__PURE__ */ new Map();
266
571
  constructor(opts) {
267
- this._store = opts.store;
572
+ if (opts.repository) this._store = repositoryAsOutboxStore(opts.repository);
573
+ else if (opts.store) this._store = opts.store;
574
+ 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
575
  this._transport = opts.transport;
269
576
  this._batchSize = opts.batchSize ?? 100;
270
577
  this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
271
578
  this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
272
579
  this._onError = opts.onError;
273
580
  this._usePublishMany = opts.usePublishMany ?? true;
581
+ this._failurePolicy = opts.failurePolicy;
274
582
  }
275
583
  /** Unique consumer ID used for lease ownership when the store supports claims */
276
584
  get consumerId() {
@@ -292,7 +600,11 @@ var EventOutbox = class {
292
600
  if (!event || typeof event !== "object") throw new InvalidOutboxEventError("event is not an object");
293
601
  if (!event.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
294
602
  if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
295
- await this._store.save(event, options);
603
+ const effectiveOptions = options?.dedupeKey === void 0 && event.meta.idempotencyKey ? {
604
+ ...options ?? {},
605
+ dedupeKey: event.meta.idempotencyKey
606
+ } : options;
607
+ await this._store.save(event, effectiveOptions);
296
608
  }
297
609
  _reportError(kind, error, event) {
298
610
  if (!this._onError) return;
@@ -357,6 +669,7 @@ var EventOutbox = class {
357
669
  ownershipMismatches: 0,
358
670
  malformed: 0,
359
671
  failHookErrors: 0,
672
+ deadLettered: 0,
360
673
  usedPublishMany: false
361
674
  };
362
675
  if (!this._transport) return empty;
@@ -380,7 +693,8 @@ var EventOutbox = class {
380
693
  publishFailed: 0,
381
694
  ackFailed: 0,
382
695
  ownershipMismatches: 0,
383
- failHookErrors: 0
696
+ failHookErrors: 0,
697
+ deadLettered: 0
384
698
  };
385
699
  const canPublishMany = this._usePublishMany && typeof this._transport.publishMany === "function";
386
700
  const canFail = typeof this._store.fail === "function";
@@ -414,8 +728,28 @@ var EventOutbox = class {
414
728
  stopBatch = true;
415
729
  continue;
416
730
  }
731
+ const attempts = (this._attempts.get(event.meta.id) ?? 0) + 1;
732
+ this._attempts.set(event.meta.id, attempts);
733
+ let failOpts = { consumerId: this._consumerId };
734
+ if (this._failurePolicy) try {
735
+ const decision = await this._failurePolicy({
736
+ event,
737
+ error: publishErr,
738
+ attempts
739
+ });
740
+ failOpts = {
741
+ ...failOpts,
742
+ ...decision
743
+ };
744
+ } catch (policyErr) {
745
+ this._reportError("fail_failed", policyErr, event);
746
+ }
417
747
  try {
418
- await this._store.fail(event.meta.id, normalizeError(publishErr), { consumerId: this._consumerId });
748
+ await this._store.fail(event.meta.id, normalizeError(publishErr), failOpts);
749
+ if (failOpts.deadLetter) {
750
+ counts.deadLettered++;
751
+ this._attempts.delete(event.meta.id);
752
+ }
419
753
  } catch (failErr) {
420
754
  if (failErr instanceof OutboxOwnershipError) {
421
755
  counts.ownershipMismatches++;
@@ -430,6 +764,7 @@ var EventOutbox = class {
430
764
  try {
431
765
  await this._store.acknowledge(event.meta.id, { consumerId: this._consumerId });
432
766
  counts.relayed++;
767
+ this._attempts.delete(event.meta.id);
433
768
  } catch (ackErr) {
434
769
  counts.ackFailed++;
435
770
  if (ackErr instanceof OutboxOwnershipError) {
@@ -446,10 +781,24 @@ var EventOutbox = class {
446
781
  ownershipMismatches: counts.ownershipMismatches,
447
782
  malformed,
448
783
  failHookErrors: counts.failHookErrors,
784
+ deadLettered: counts.deadLettered,
449
785
  usedPublishMany: canPublishMany && valid.length > 0
450
786
  };
451
787
  }
452
788
  /**
789
+ * Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
790
+ * envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
791
+ * `[]` when the store doesn't implement it.
792
+ *
793
+ * Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
794
+ * `deadLetter: true` after N attempts, then read back with this to alert,
795
+ * replay, or archive.
796
+ */
797
+ async getDeadLettered(limit = 100) {
798
+ if (!this._store.getDeadLettered) return [];
799
+ return this._store.getDeadLettered(limit);
800
+ }
801
+ /**
453
802
  * Purge old **delivered** events from the outbox store.
454
803
  * Delegates to `store.purge()` if implemented; no-op otherwise.
455
804
  * @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
@@ -468,101 +817,6 @@ function normalizeError(err) {
468
817
  return { message: String(err) };
469
818
  }
470
819
  /**
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
820
  * Compute a `retryAt` `Date` using exponential backoff with jitter.
567
821
  *
568
822
  * This is a convenience helper for store authors implementing
@@ -606,4 +860,4 @@ function exponentialBackoff(options) {
606
860
  return new Date(now + jittered);
607
861
  }
608
862
  //#endregion
609
- export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
863
+ export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createChildEvent, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, repositoryAsOutboxStore, 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-CakIQmwR.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-CUw5NNWe.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-Co8k3NyS.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-BuvPma24.mjs";
2
+ import { t as loadResources } from "../loadResources-BAzJItAJ.mjs";
3
3
  //#region src/factory/edge.ts
4
4
  /**
5
5
  * Convert a Fastify app into a Web Standards fetch handler.
@@ -175,4 +175,124 @@ interface PermissionCheckMeta {
175
175
  _orgInScopeTarget?: string | ((ctx: PermissionContext) => string | undefined);
176
176
  }
177
177
  //#endregion
178
- export { getUserRoles as a, UserBase as i, PermissionContext as n, normalizeRoles as o, PermissionResult as r, PermissionCheck as t };
178
+ //#region src/permissions/fields.d.ts
179
+ /**
180
+ * Field-Level Permissions
181
+ *
182
+ * Control field visibility and writability per role.
183
+ * Integrated into the response path (read) and sanitization path (write).
184
+ *
185
+ * @example
186
+ * ```typescript
187
+ * import { fields, defineResource } from '@classytic/arc';
188
+ *
189
+ * const userResource = defineResource({
190
+ * name: 'user',
191
+ * adapter: userAdapter,
192
+ * fields: {
193
+ * salary: fields.visibleTo(['admin', 'hr']),
194
+ * internalNotes: fields.writableBy(['admin']),
195
+ * email: fields.redactFor(['viewer']),
196
+ * password: fields.hidden(),
197
+ * },
198
+ * });
199
+ * ```
200
+ */
201
+ type FieldPermissionType = "hidden" | "visibleTo" | "writableBy" | "redactFor";
202
+ interface FieldPermission {
203
+ readonly _type: FieldPermissionType;
204
+ readonly roles?: readonly string[];
205
+ readonly redactValue?: unknown;
206
+ }
207
+ type FieldPermissionMap = Record<string, FieldPermission>;
208
+ declare const fields: {
209
+ /**
210
+ * Field is never included in responses. Not writable via API.
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * fields: { password: fields.hidden() }
215
+ * ```
216
+ */
217
+ hidden(): FieldPermission;
218
+ /**
219
+ * Field is only visible to users with specified roles.
220
+ * Other users don't see the field at all.
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * fields: { salary: fields.visibleTo(['admin', 'hr']) }
225
+ * ```
226
+ */
227
+ visibleTo(roles: readonly string[]): FieldPermission;
228
+ /**
229
+ * Field is only writable by users with specified roles.
230
+ * All users can still read the field. Users without the role
231
+ * have the field silently stripped from write operations.
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * fields: { role: fields.writableBy(['admin']) }
236
+ * ```
237
+ */
238
+ writableBy(roles: readonly string[]): FieldPermission;
239
+ /**
240
+ * Field is redacted (replaced with a placeholder) for specified roles.
241
+ * Other users see the real value.
242
+ *
243
+ * @param roles - Roles that see the redacted value
244
+ * @param redactValue - Replacement value (default: '***')
245
+ *
246
+ * @example
247
+ * ```typescript
248
+ * fields: {
249
+ * email: fields.redactFor(['viewer']),
250
+ * ssn: fields.redactFor(['basic'], '***-**-****'),
251
+ * }
252
+ * ```
253
+ */
254
+ redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
255
+ };
256
+ /**
257
+ * Apply field-level READ permissions to a response object.
258
+ * Strips hidden fields, enforces visibility, and applies redaction.
259
+ *
260
+ * @param data - The response object (mutated in place for performance)
261
+ * @param fieldPermissions - Field permission map from resource config
262
+ * @param userRoles - Current user's roles (empty array for unauthenticated)
263
+ * @returns The filtered object
264
+ */
265
+ declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
266
+ /**
267
+ * Result of applying write permissions — includes both the filtered body
268
+ * and the list of fields that were stripped so callers can decide whether
269
+ * to reject the request (secure default) or silently strip (legacy).
270
+ */
271
+ interface FieldWritePermissionResult<T extends Record<string, unknown>> {
272
+ readonly body: T;
273
+ readonly deniedFields: readonly string[];
274
+ }
275
+ /**
276
+ * Apply field-level WRITE permissions to request body.
277
+ *
278
+ * Returns both the filtered body and the list of denied fields. Callers are
279
+ * expected to reject the request when `deniedFields.length > 0` — silently
280
+ * stripping fields hides misconfigurations and real attacks. See
281
+ * `BodySanitizer` for the default policy.
282
+ *
283
+ * @param body - The request body (returns a new filtered copy)
284
+ * @param fieldPermissions - Field permission map from resource config
285
+ * @param userRoles - Current user's roles
286
+ */
287
+ declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
288
+ /**
289
+ * Resolve effective roles by merging global user roles with org-level roles.
290
+ *
291
+ * Global roles come from `req.user.role` (normalized via getUserRoles()).
292
+ * Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
293
+ *
294
+ * When no org context exists, returns global roles only — backward compatible.
295
+ */
296
+ declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
297
+ //#endregion
298
+ export { applyFieldWritePermissions as a, PermissionCheck as c, UserBase as d, getUserRoles as f, applyFieldReadPermissions as i, PermissionContext as l, FieldPermissionMap as n, fields as o, normalizeRoles as p, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t, PermissionResult as u };