@classytic/arc 2.8.4 → 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 (130) hide show
  1. package/README.md +116 -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 +15 -7
  8. package/dist/auth/index.mjs +13 -6
  9. package/dist/{betterAuthOpenApi-C5lDyRH2.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
  10. package/dist/cache/index.d.mts +71 -1
  11. package/dist/cache/index.mjs +96 -3
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/core/index.d.mts +3 -3
  15. package/dist/core/index.mjs +4 -5
  16. package/dist/{core-DKSwNSXf.mjs → core-DNncu0xF.mjs} +1 -1
  17. package/dist/{createActionRouter-Df1BuawX.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
  18. package/dist/{createApp-BOYjBgdI.mjs → createApp-CBJUJKGP.mjs} +6 -5
  19. package/dist/{defineResource-Bb_Bdhtw.mjs → defineResource-C__jkwvs.mjs} +22 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/dynamic/index.d.mts +2 -2
  23. package/dist/dynamic/index.mjs +3 -3
  24. package/dist/{elevation-BBGFjzIP.mjs → elevation-DxQ6ACbt.mjs} +20 -6
  25. package/dist/{errorHandler-mzqk4cGl.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
  26. package/dist/{errorHandler-CdZDavNH.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
  27. package/dist/{eventPlugin-CVxlE6De.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
  28. package/dist/{eventPlugin-D91S2YF4.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
  29. package/dist/events/index.d.mts +147 -36
  30. package/dist/events/index.mjs +338 -101
  31. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  32. package/dist/events/transports/redis.d.mts +1 -1
  33. package/dist/factory/index.d.mts +1 -1
  34. package/dist/factory/index.mjs +1 -1
  35. package/dist/{fields-DC4So2M2.d.mts → fields-BC7zcmI9.d.mts} +15 -3
  36. package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
  37. package/dist/filesUpload-q8oHt--L.mjs +377 -0
  38. package/dist/hooks/index.d.mts +1 -1
  39. package/dist/idempotency/index.d.mts +28 -4
  40. package/dist/idempotency/index.mjs +111 -2
  41. package/dist/idempotency/redis.d.mts +2 -2
  42. package/dist/idempotency/redis.mjs +134 -13
  43. package/dist/{index-CSkeivBx.d.mts → index-C-xjcA6F.d.mts} +2 -2
  44. package/dist/{index-CpTSDqmD.d.mts → index-Cibkchnx.d.mts} +5 -136
  45. package/dist/{index-BgmMdpm8.d.mts → index-CtGKT0lf.d.mts} +1 -1
  46. package/dist/index.d.mts +8 -8
  47. package/dist/index.mjs +8 -8
  48. package/dist/integrations/event-gateway.d.mts +1 -1
  49. package/dist/integrations/index.d.mts +1 -1
  50. package/dist/integrations/jobs.d.mts +25 -3
  51. package/dist/integrations/jobs.mjs +63 -4
  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-BVuMfeVv.d.mts → interface-YrWsmKqE.d.mts} +324 -194
  59. package/dist/{openapi-CYCuekCn.mjs → openapi-CXuTG1M9.mjs} +3 -3
  60. package/dist/org/index.d.mts +2 -2
  61. package/dist/permissions/index.d.mts +3 -3
  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 +6 -6
  65. package/dist/plugins/index.mjs +4 -4
  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 +26 -33
  70. package/dist/presets/filesUpload.d.mts +71 -0
  71. package/dist/presets/filesUpload.mjs +2 -0
  72. package/dist/presets/index.d.mts +4 -2
  73. package/dist/presets/index.mjs +4 -2
  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-D0iIVhW_.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
  80. package/dist/redis-MXLp1oOf.d.mts +115 -0
  81. package/dist/{redis-stream-D54N5oXs.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
  82. package/dist/registry/index.d.mts +1 -1
  83. package/dist/{resourceToTools-O_HwWXFa.mjs → resourceToTools-C3cWymnW.mjs} +65 -48
  84. package/dist/rpc/index.mjs +1 -1
  85. package/dist/{schemaConverter-OxfCshus.mjs → schemaConverter-BxFDdtXu.mjs} +25 -9
  86. package/dist/scope/index.d.mts +2 -2
  87. package/dist/scope/index.mjs +1 -1
  88. package/dist/storage-BwGQXUpd.d.mts +146 -0
  89. package/dist/store-helpers-DFiZl5TL.mjs +57 -0
  90. package/dist/testing/index.d.mts +7 -15
  91. package/dist/testing/index.mjs +23 -76
  92. package/dist/testing/storageContract.d.mts +26 -0
  93. package/dist/testing/storageContract.mjs +216 -0
  94. package/dist/types/index.d.mts +5 -5
  95. package/dist/types/storage.d.mts +2 -0
  96. package/dist/types/storage.mjs +1 -0
  97. package/dist/{types-CcG4avic.d.mts → types-CoSzA-s-.d.mts} +1 -1
  98. package/dist/{types-Bg2X42_m.d.mts → types-CunEX4UX.d.mts} +7 -5
  99. package/dist/{types-CVC4HOKi.d.mts → types-DZi1aYhm.d.mts} +1 -1
  100. package/dist/utils/index.d.mts +26 -8
  101. package/dist/utils/index.mjs +6 -6
  102. package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
  103. package/package.json +23 -11
  104. package/skills/arc/SKILL.md +92 -14
  105. package/skills/arc/references/auth.md +94 -0
  106. package/skills/arc/references/events.md +229 -12
  107. package/skills/arc/references/mcp.md +4 -17
  108. package/skills/arc/references/multi-tenancy.md +43 -0
  109. package/skills/arc/references/production.md +34 -19
  110. package/dist/EventTransport-CinyO7zQ.d.mts +0 -135
  111. package/dist/audit/mongodb.d.mts +0 -2
  112. package/dist/audit/mongodb.mjs +0 -2
  113. package/dist/idempotency/mongodb.d.mts +0 -2
  114. package/dist/idempotency/mongodb.mjs +0 -123
  115. package/dist/mongodb-B5O6xaW1.mjs +0 -90
  116. package/dist/mongodb-B8U2xaLj.d.mts +0 -127
  117. package/dist/mongodb-X7LbEjTN.d.mts +0 -80
  118. package/dist/redis-z3sFr1UP.d.mts +0 -49
  119. /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
  120. /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
  121. /package/dist/{elevation-s5ykdNHr.d.mts → elevation-B6S5csVA.d.mts} +0 -0
  122. /package/dist/{errors-Bmn3eZT6.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  123. /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
  124. /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
  125. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-CWP6MB39.mjs} +0 -0
  126. /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
  127. /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
  128. /package/dist/{tracing-DxjKk7eW.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  129. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
  130. /package/dist/{types-C72d3NDn.d.mts → types-BD85MlEK.d.mts} +0 -0
@@ -537,7 +537,7 @@ Use to verify the MCP server is alive before configuring Claude CLI.
537
537
 
538
538
  ### ArcRequest — Typed Fastify Request
539
539
 
540
- For `wrapHandler: false` routes, use `ArcRequest` instead of `(req as any).user`:
540
+ For `raw: true` routes, use `ArcRequest` instead of `(req as any).user`:
541
541
 
542
542
  ```typescript
543
543
  import type { ArcRequest } from '@classytic/arc';
@@ -589,28 +589,15 @@ throw createDomainError('INSUFFICIENT_BALANCE', 'Not enough credits', 402, { bal
589
589
  // Arc's error handler auto-maps statusCode to HTTP response
590
590
  ```
591
591
 
592
- ### onRegister — Resource Lifecycle Hook
593
-
594
- Called during plugin registration with the scoped Fastify instance:
595
-
596
- ```typescript
597
- defineResource({
598
- name: 'notification',
599
- onRegister: (fastify) => {
600
- setSseManager(fastify.sseManager);
601
- },
602
- })
603
- ```
604
-
605
592
  ### preAuth — Pre-Auth Handlers for SSE/WebSocket
606
593
 
607
594
  Run before auth middleware. Use for promoting `?token=` to `Authorization` header (EventSource can't set headers):
608
595
 
609
596
  ```typescript
610
- additionalRoutes: [{
597
+ routes: [{
611
598
  method: 'GET',
612
599
  path: '/stream',
613
- wrapHandler: false,
600
+ raw: true,
614
601
  permissions: requireAuth(),
615
602
  preAuth: [(req) => {
616
603
  const token = req.query?.token;
@@ -625,7 +612,7 @@ additionalRoutes: [{
625
612
  Auto-sets SSE headers and bypasses Arc's response wrapper:
626
613
 
627
614
  ```typescript
628
- additionalRoutes: [{
615
+ routes: [{
629
616
  method: 'POST',
630
617
  path: '/stream',
631
618
  streamResponse: true, // SSE headers + no { success, data } wrapper
@@ -178,6 +178,49 @@ Arc does NOT auto-derive scope from `request.user.organizationId` — that's a f
178
178
  2. **`elevated` with no `organizationId` bypasses tenant filtering.** This is intentional (admin sees everything), but it means you can't use `kind: 'elevated'` for normal per-org access.
179
179
  3. **`systemManaged` is your seatbelt for create/update.** Mark tenant fields (`companyId`, `organizationId`) as `systemManaged` in `fieldRules` so `BodySanitizer` strips any client-supplied value. The tenant field is then injected from the scope at write time — not from the request body.
180
180
  4. **Rate-limit keys respect all 5 scope kinds.** The built-in `createTenantKeyGenerator` uses `organizationId` for member/service/elevated and falls back to `userId`/IP for authenticated/public.
181
+ 5. **multiTenant preset injects org on UPDATE (v2.9).** Body-supplied `organizationId` is overwritten with the caller's scope — closes the tenant-hop vector where a member could PATCH their own doc into another tenant. Elevated scope with no org still bypasses (admin cross-tenant).
182
+
183
+ ## Mongokit tenant-context helper (optional)
184
+
185
+ If your adapter is mongokit ≥3.7, you can wire mongokit's `createTenantContext()` to propagate the org id through `AsyncLocalStorage` — useful when domain code outside arc routes needs the current tenant without threading `req` everywhere:
186
+
187
+ ```ts
188
+ import { createTenantContext, multiTenantPlugin, Repository } from '@classytic/mongokit';
189
+ import { getOrgId } from '@classytic/arc/scope';
190
+
191
+ const tenantContext = createTenantContext();
192
+ const repo = new Repository(Model, [
193
+ multiTenantPlugin({ tenantField: 'organizationId', context: tenantContext }),
194
+ ]);
195
+
196
+ // Install scope → ALS bridge once in your app:
197
+ fastify.addHook('preHandler', (req, _reply, done) => {
198
+ const scope = (req.metadata as { _scope?: unknown } | undefined)?._scope;
199
+ const orgId = scope ? getOrgId(scope as never) : undefined;
200
+ if (orgId) tenantContext.run(orgId, done);
201
+ else done();
202
+ });
203
+
204
+ // Now any domain code can read it:
205
+ import { getTenantId } from './tenantContext.js';
206
+ await someService.doThing({ orgId: getTenantId() });
207
+ ```
208
+
209
+ Arc doesn't bundle this — it's mongokit-specific. Arc's scope helpers (`getOrgId`, `getOrgContext`) remain the source of truth inside the request cycle; `createTenantContext()` is a complement for code that lives outside it.
210
+
211
+ ## Plugin-order safety (mongokit ≥3.7)
212
+
213
+ Mongokit's `Repository` constructor accepts `pluginOrderChecks: 'warn' | 'throw' | 'off'` (default `'warn'`). Pass `'throw'` in production to catch foot-guns — e.g. installing `softDeletePlugin` AFTER `batchOperationsPlugin` silently bypasses soft-delete on `deleteMany`:
214
+
215
+ ```ts
216
+ new Repository(Model, [
217
+ multiTenantPlugin({...}), // must precede cache + batch-ops
218
+ softDeletePlugin(), // must precede batch-ops
219
+ batchOperationsPlugin(),
220
+ ], {}, { pluginOrderChecks: 'throw' });
221
+ ```
222
+
223
+ Arc doesn't surface this option — configure it directly on the mongokit `Repository` you hand to `defineResource({ adapter: createMongooseAdapter({ repository }) })`.
181
224
 
182
225
  ## Helpers reference
183
226
 
@@ -2,6 +2,15 @@
2
2
 
3
3
  Health checks, audit trail, idempotency, tracing, SSE, caching, graceful shutdown.
4
4
 
5
+ ## v2.9 Security Defaults
6
+
7
+ - **Field-write perms reject by default** — `defineResource({ fields, onFieldWriteDenied })`. Default `'reject'` → 403 with denied field list. Legacy silent-strip: `onFieldWriteDenied: 'strip'`.
8
+ - **Elevation always emits `arc.scope.elevated`** — subscribe via `fastify.events.subscribe('arc.scope.elevated', handler)` for audit. `onElevation` callback still supported.
9
+ - **multiTenant injects org on UPDATE** — closes cross-tenant hop vector. Body-supplied `organizationId` overwritten with caller's scope.
10
+ - **`verifySignature(body, secret, sig)` throws TypeError** if body isn't string/Buffer — register `@fastify/raw-body` before webhook routes; pass `req.rawBody`.
11
+ - **Upload `sanitizeFilename` (preset)** — strict by default; `false` / `'*'` / custom fn to relax per adapter needs.
12
+ - **Idempotency `namespace`** — fold an env key into the fingerprint when multiple deployments share a Redis (prod + canary, api + jobs).
13
+
5
14
  ## Health Plugin
6
15
 
7
16
  Kubernetes-ready liveness/readiness probes:
@@ -56,22 +65,21 @@ await fastify.register(gracefulShutdownPlugin, {
56
65
 
57
66
  ## Audit Plugin
58
67
 
59
- Change tracking with pluggable storage:
68
+ Change tracking. Pass a `RepositoryLike` for persistence, or omit for in-memory dev:
60
69
 
61
70
  ```typescript
62
71
  import { auditPlugin } from '@classytic/arc/audit';
72
+ import { Repository } from '@classytic/mongokit';
63
73
 
64
- // Development
65
- await fastify.register(auditPlugin, { enabled: true, stores: ['memory'] });
74
+ // Development (in-memory)
75
+ await fastify.register(auditPlugin, { enabled: true });
66
76
 
67
- // Production
77
+ // Production — any kit (mongokit / prismakit / custom)
68
78
  await fastify.register(auditPlugin, {
69
79
  enabled: true,
70
- stores: ['mongodb'],
71
- mongoConnection: mongoose.connection,
72
- mongoCollection: 'audit_logs',
73
- ttlDays: 90, // Auto-cleanup via TTL index
80
+ repository: new Repository(AuditModel),
74
81
  });
82
+ // TTL / retention owned by your DB (TTL index on `timestamp`, cron DELETE, etc.)
75
83
 
76
84
  // Usage
77
85
  await fastify.audit.create('product', product._id, product, request.auditContext);
@@ -137,16 +145,18 @@ fetch('/api/orders', {
137
145
  **Storage backends:**
138
146
 
139
147
  ```typescript
140
- // Memory (default, dev)
148
+ // Memory (default, dev) — omit both `repository` and `store`
141
149
  import { MemoryIdempotencyStore } from '@classytic/arc/idempotency';
142
150
 
143
151
  // Redis (production, multi-instance)
144
152
  import { RedisIdempotencyStore } from '@classytic/arc/idempotency/redis';
145
153
  store: new RedisIdempotencyStore({ client: redis, prefix: 'idem:', ttlMs: 86400000 })
146
154
 
147
- // MongoDB (production, no Redis)
148
- import { MongoIdempotencyStore } from '@classytic/arc/idempotency/mongodb';
149
- store: new MongoIdempotencyStore({ connection: mongoose.connection, collection: 'arc_idempotency', createIndex: true })
155
+ // DB-backed via RepositoryLike (mongokit / prismakit / custom)
156
+ import { Repository, methodRegistryPlugin, batchOperationsPlugin, mongoOperationsPlugin } from '@classytic/mongokit';
157
+ repository: new Repository(IdempotencyModel, [
158
+ methodRegistryPlugin(), batchOperationsPlugin(), mongoOperationsPlugin(),
159
+ ])
150
160
  ```
151
161
 
152
162
  **IdempotencyStore interface:**
@@ -532,20 +542,25 @@ Transactional outbox pattern — at-least-once delivery even if transport is dow
532
542
 
533
543
  ```typescript
534
544
  import { EventOutbox, MemoryOutboxStore } from '@classytic/arc/events';
545
+ import { Repository } from '@classytic/mongokit';
546
+
547
+ // Dev
548
+ const outbox = new EventOutbox({ store: new MemoryOutboxStore(), transport: redisTransport });
535
549
 
550
+ // Production — any RepositoryLike
536
551
  const outbox = new EventOutbox({
537
- store: new MemoryOutboxStore(), // or MongoOutboxStore for production
552
+ repository: new Repository(OutboxModel),
538
553
  transport: redisTransport,
539
554
  });
540
555
 
541
- // In business logic (same DB transaction)
542
- await outbox.store(event);
556
+ // In business logic (same DB transaction via `{ session }`)
557
+ await outbox.store(event, { session });
543
558
 
544
559
  // Relay cron (runs every few seconds)
545
560
  const relayed = await outbox.relay(); // publishes pending → transport
546
561
  ```
547
562
 
548
- **OutboxStore interface**: `save(event)`, `getPending(limit)`, `acknowledge(eventId)`.
563
+ **OutboxStore interface**: `save(event)`, `getPending(limit)`, `acknowledge(eventId)` (+ optional `claimPending`, `fail`, `getDeadLettered`, `purge`). When you pass `repository`, arc adapts it internally.
549
564
 
550
565
  ## RPC Service Client — Schema Versioning
551
566
 
@@ -655,16 +670,16 @@ await withCompensation('checkout', steps, { orderId }, {
655
670
  });
656
671
  ```
657
672
 
658
- **In an additionalRoute with Arc auth:**
673
+ **In a custom route with Arc auth:**
659
674
 
660
675
  ```typescript
661
676
  defineResource({
662
677
  name: 'order',
663
- additionalRoutes: [{
678
+ routes: [{
664
679
  method: 'POST',
665
680
  path: '/:id/checkout',
666
681
  permissions: requireAuth(),
667
- wrapHandler: false,
682
+ raw: true,
668
683
  handler: async (request, reply) => {
669
684
  const result = await withCompensation('checkout', steps, { orderId: request.params.id });
670
685
  if (!result.success) return reply.code(422).send({ error: result.error });
@@ -1,135 +0,0 @@
1
- //#region src/events/EventTransport.d.ts
2
- /**
3
- * Event Transport Interface
4
- *
5
- * Defines contract for event delivery backends.
6
- * Implement for durable transports (Redis, RabbitMQ, Kafka, etc.)
7
- *
8
- * @example
9
- * // Redis Pub/Sub implementation
10
- * class RedisEventTransport implements EventTransport {
11
- * async publish(event) {
12
- * await redis.publish(event.type, JSON.stringify(event));
13
- * }
14
- * async subscribe(pattern, handler) {
15
- * redis.psubscribe(pattern);
16
- * redis.on('pmessage', (p, channel, msg) => handler(JSON.parse(msg)));
17
- * }
18
- * }
19
- */
20
- interface DomainEvent<T = unknown> {
21
- /** Event type (e.g., 'product.created', 'order.shipped') */
22
- type: string;
23
- /** Event payload */
24
- payload: T;
25
- /** Event metadata */
26
- meta: {
27
- /** Unique event ID */id: string; /** Event timestamp */
28
- timestamp: Date; /** Source resource */
29
- resource?: string; /** Resource ID */
30
- resourceId?: string; /** User who triggered the event */
31
- userId?: string; /** Organization context */
32
- organizationId?: string; /** Correlation ID for tracing */
33
- correlationId?: string;
34
- };
35
- }
36
- type EventHandler<T = unknown> = (event: DomainEvent<T>) => void | Promise<void>;
37
- /**
38
- * Minimal logger interface for event transports.
39
- * Compatible with `console`, `pino`, `fastify.log`, and any custom logger.
40
- *
41
- * @example
42
- * ```typescript
43
- * // Use Fastify's logger
44
- * new MemoryEventTransport({ logger: fastify.log });
45
- *
46
- * // Use a custom logger
47
- * new MemoryEventTransport({ logger: { warn: myWarn, error: myError } });
48
- *
49
- * // Default: console (no logger option needed)
50
- * new MemoryEventTransport();
51
- * ```
52
- */
53
- interface EventLogger {
54
- warn(message: string, ...args: unknown[]): void;
55
- error(message: string, ...args: unknown[]): void;
56
- }
57
- interface EventTransport {
58
- /** Transport name for logging */
59
- readonly name: string;
60
- /**
61
- * Publish an event to the transport
62
- */
63
- publish(event: DomainEvent): Promise<void>;
64
- /**
65
- * Publish a batch of events to the transport (optional, v2.8.1+).
66
- *
67
- * Transports that can efficiently batch (Kafka producer, Redis pipeline,
68
- * RabbitMQ publisher confirms, SQS send-message-batch) should implement
69
- * this. {@link import('./outbox.js').EventOutbox.relay} auto-detects and
70
- * uses it for much higher throughput than per-event publishing.
71
- *
72
- * **Contract**: the returned `PublishManyResult` must describe the
73
- * per-event outcome so the caller can acknowledge successes and fail the
74
- * rest. Partial success is allowed — the transport reports it per event.
75
- *
76
- * If not implemented, `EventOutbox.relay` falls back to calling
77
- * {@link publish} once per event.
78
- *
79
- * @param events - Events to publish (in order)
80
- * @returns Per-event outcome map keyed by `event.meta.id`
81
- */
82
- publishMany?(events: readonly DomainEvent[]): Promise<PublishManyResult>;
83
- /**
84
- * Subscribe to events matching a pattern
85
- * @param pattern - Event type pattern (e.g., 'product.*', '*')
86
- * @param handler - Handler function
87
- * @returns Unsubscribe function
88
- */
89
- subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
90
- /**
91
- * Close transport connections
92
- */
93
- close?(): Promise<void>;
94
- }
95
- /**
96
- * Per-event outcome returned by {@link EventTransport.publishMany}.
97
- *
98
- * The key is `event.meta.id`; the value is `null` for success or an `Error`
99
- * for per-event failure. Transports MUST include an entry for every event
100
- * in the input batch.
101
- */
102
- type PublishManyResult = ReadonlyMap<string, Error | null>;
103
- interface MemoryEventTransportOptions {
104
- /** Logger for error/warning messages (default: console) */
105
- logger?: EventLogger;
106
- }
107
- /**
108
- * In-memory event transport (default)
109
- * Events are delivered synchronously within the process.
110
- * Not suitable for multi-instance deployments.
111
- */
112
- declare class MemoryEventTransport implements EventTransport {
113
- readonly name = "memory";
114
- private handlers;
115
- private logger;
116
- constructor(options?: MemoryEventTransportOptions);
117
- publish(event: DomainEvent): Promise<void>;
118
- /**
119
- * Reference `publishMany` implementation — delegates to `publish()` in order.
120
- *
121
- * Production transports (Kafka, Redis pipeline, SQS batch) should override
122
- * this with a single batched network call. Memory transport has nothing to
123
- * batch, so we just loop — the loop still returns a proper result map so
124
- * `EventOutbox.relay` can exercise the batched code path in tests.
125
- */
126
- publishMany(events: readonly DomainEvent[]): Promise<PublishManyResult>;
127
- subscribe(pattern: string, handler: EventHandler): Promise<() => void>;
128
- close(): Promise<void>;
129
- }
130
- /**
131
- * Create a domain event with auto-generated metadata
132
- */
133
- declare function createEvent<T>(type: string, payload: T, meta?: Partial<DomainEvent["meta"]>): DomainEvent<T>;
134
- //#endregion
135
- export { MemoryEventTransport as a, createEvent as c, EventTransport as i, EventHandler as n, MemoryEventTransportOptions as o, EventLogger as r, PublishManyResult as s, DomainEvent as t };
@@ -1,2 +0,0 @@
1
- import { n as MongoAuditStoreOptions, t as MongoAuditStore } from "../mongodb-B8U2xaLj.mjs";
2
- export { MongoAuditStore, type MongoAuditStoreOptions };
@@ -1,2 +0,0 @@
1
- import { t as MongoAuditStore } from "../mongodb-B5O6xaW1.mjs";
2
- export { MongoAuditStore };
@@ -1,2 +0,0 @@
1
- import { n as MongoIdempotencyStoreOptions, t as MongoIdempotencyStore } from "../mongodb-X7LbEjTN.mjs";
2
- export { MongoIdempotencyStore, type MongoIdempotencyStoreOptions };
@@ -1,123 +0,0 @@
1
- //#region src/idempotency/stores/mongodb.ts
2
- var MongoIdempotencyStore = class {
3
- name = "mongodb";
4
- connection;
5
- collectionName;
6
- ttlMs;
7
- indexCreated = false;
8
- shouldEnsureIndex;
9
- logger;
10
- constructor(options) {
11
- this.connection = options.connection;
12
- this.collectionName = options.collection ?? "arc_idempotency";
13
- this.ttlMs = options.ttlMs ?? 864e5;
14
- this.shouldEnsureIndex = options.createIndex !== false;
15
- this.logger = options.logger ?? console;
16
- if (this.shouldEnsureIndex) this.ensureIndex().catch(() => {});
17
- }
18
- get collection() {
19
- return this.connection.db.collection(this.collectionName);
20
- }
21
- async ensureIndex() {
22
- if (this.indexCreated) return;
23
- try {
24
- await this.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
25
- this.indexCreated = true;
26
- } catch (err) {
27
- const code = err?.code;
28
- if (code === 85 || code === 86) {
29
- this.indexCreated = true;
30
- return;
31
- }
32
- this.logger.warn(`[MongoIdempotencyStore] TTL index creation failed (will retry on next write): ${err.message ?? err}`);
33
- }
34
- }
35
- async get(key) {
36
- const doc = await this.collection.findOne({ _id: key });
37
- if (!doc?.result) return void 0;
38
- if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return;
39
- return {
40
- key,
41
- statusCode: doc.result.statusCode,
42
- headers: doc.result.headers,
43
- body: doc.result.body,
44
- createdAt: new Date(doc.createdAt),
45
- expiresAt: new Date(doc.expiresAt)
46
- };
47
- }
48
- async set(key, result) {
49
- if (this.shouldEnsureIndex && !this.indexCreated) await this.ensureIndex().catch(() => {});
50
- await this.collection.updateOne({ _id: key }, {
51
- $set: {
52
- result: {
53
- statusCode: result.statusCode,
54
- headers: result.headers,
55
- body: result.body
56
- },
57
- createdAt: result.createdAt,
58
- expiresAt: result.expiresAt
59
- },
60
- $unset: { lock: "" }
61
- }, { upsert: true });
62
- }
63
- async tryLock(key, requestId, ttlMs) {
64
- if (this.shouldEnsureIndex && !this.indexCreated) await this.ensureIndex().catch(() => {});
65
- const now = /* @__PURE__ */ new Date();
66
- const expiresAt = new Date(now.getTime() + ttlMs);
67
- try {
68
- const result = await this.collection.updateOne({
69
- _id: key,
70
- $or: [{ lock: { $exists: false } }, { "lock.expiresAt": { $lt: now } }]
71
- }, {
72
- $set: { lock: {
73
- requestId,
74
- expiresAt
75
- } },
76
- $setOnInsert: {
77
- createdAt: now,
78
- expiresAt: new Date(now.getTime() + this.ttlMs)
79
- }
80
- }, { upsert: true });
81
- return result.matchedCount === 1 || (result.upsertedCount ?? 0) === 1;
82
- } catch (err) {
83
- if (err?.code === 11e3) return false;
84
- throw err;
85
- }
86
- }
87
- async unlock(key, requestId) {
88
- await this.collection.updateOne({
89
- _id: key,
90
- "lock.requestId": requestId
91
- }, { $unset: { lock: "" } });
92
- }
93
- async isLocked(key) {
94
- const doc = await this.collection.findOne({ _id: key });
95
- if (!doc?.lock) return false;
96
- return new Date(doc.lock.expiresAt) > /* @__PURE__ */ new Date();
97
- }
98
- async delete(key) {
99
- await this.collection.deleteOne({ _id: key });
100
- }
101
- async deleteByPrefix(prefix) {
102
- return (await this.collection.deleteMany({ _id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` } })).deletedCount;
103
- }
104
- async findByPrefix(prefix) {
105
- const doc = await this.collection.findOne({
106
- _id: { $regex: `^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}` },
107
- result: { $exists: true },
108
- expiresAt: { $gt: /* @__PURE__ */ new Date() }
109
- });
110
- if (!doc?.result) return void 0;
111
- return {
112
- key: doc._id,
113
- statusCode: doc.result.statusCode,
114
- headers: doc.result.headers,
115
- body: doc.result.body,
116
- createdAt: new Date(doc.createdAt),
117
- expiresAt: new Date(doc.expiresAt)
118
- };
119
- }
120
- async close() {}
121
- };
122
- //#endregion
123
- export { MongoIdempotencyStore };
@@ -1,90 +0,0 @@
1
- //#region src/audit/stores/mongodb.ts
2
- var MongoAuditStore = class {
3
- name = "mongodb";
4
- collection;
5
- initialized = false;
6
- ttlDays;
7
- constructor(options) {
8
- const collectionName = options.collection ?? "audit_logs";
9
- this.collection = options.connection.collection(collectionName);
10
- this.ttlDays = options.ttlDays ?? 90;
11
- }
12
- async ensureIndexes() {
13
- if (this.initialized) return;
14
- try {
15
- await this.collection.createIndex({
16
- resource: 1,
17
- documentId: 1,
18
- timestamp: -1
19
- });
20
- await this.collection.createIndex({
21
- userId: 1,
22
- timestamp: -1
23
- });
24
- await this.collection.createIndex({
25
- organizationId: 1,
26
- timestamp: -1
27
- });
28
- if (this.ttlDays > 0) await this.collection.createIndex({ timestamp: 1 }, { expireAfterSeconds: this.ttlDays * 24 * 60 * 60 });
29
- this.initialized = true;
30
- } catch {
31
- this.initialized = true;
32
- }
33
- }
34
- async log(entry) {
35
- await this.ensureIndexes();
36
- await this.collection.insertOne({
37
- _id: entry.id,
38
- resource: entry.resource,
39
- documentId: entry.documentId,
40
- action: entry.action,
41
- userId: entry.userId,
42
- organizationId: entry.organizationId,
43
- before: entry.before,
44
- after: entry.after,
45
- changes: entry.changes,
46
- requestId: entry.requestId,
47
- ipAddress: entry.ipAddress,
48
- userAgent: entry.userAgent,
49
- metadata: entry.metadata,
50
- timestamp: entry.timestamp
51
- });
52
- }
53
- async query(options = {}) {
54
- await this.ensureIndexes();
55
- const query = {};
56
- if (options.resource) query.resource = options.resource;
57
- if (options.documentId) query.documentId = options.documentId;
58
- if (options.userId) query.userId = options.userId;
59
- if (options.organizationId) query.organizationId = options.organizationId;
60
- if (options.action) {
61
- const actions = Array.isArray(options.action) ? options.action : [options.action];
62
- query.action = actions.length === 1 ? actions[0] : { $in: actions };
63
- }
64
- if (options.from || options.to) {
65
- query.timestamp = {};
66
- if (options.from) query.timestamp.$gte = options.from;
67
- if (options.to) query.timestamp.$lte = options.to;
68
- }
69
- const offset = options.offset ?? 0;
70
- const limit = options.limit ?? 100;
71
- return (await this.collection.find(query).sort({ timestamp: -1 }).skip(offset).limit(limit).toArray()).map((doc) => ({
72
- id: String(doc._id),
73
- resource: doc.resource,
74
- documentId: doc.documentId,
75
- action: doc.action,
76
- userId: doc.userId,
77
- organizationId: doc.organizationId,
78
- before: doc.before,
79
- after: doc.after,
80
- changes: doc.changes,
81
- requestId: doc.requestId,
82
- ipAddress: doc.ipAddress,
83
- userAgent: doc.userAgent,
84
- metadata: doc.metadata,
85
- timestamp: doc.timestamp
86
- }));
87
- }
88
- };
89
- //#endregion
90
- export { MongoAuditStore as t };