@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.
- package/README.md +88 -5
- package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-Vu2yc56T.mjs} +188 -102
- package/dist/EventTransport-CqZ8FyM_.d.mts +293 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +100 -11
- package/dist/audit/index.mjs +71 -18
- package/dist/auth/index.d.mts +16 -8
- package/dist/auth/index.mjs +13 -6
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -5
- package/dist/{core-F0QoWBt2.mjs → core-DNncu0xF.mjs} +1 -1
- package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
- package/dist/{createApp-B1EY8zxa.mjs → createApp-CBJUJKGP.mjs} +13 -12
- package/dist/{defineResource-tcgySDo1.mjs → defineResource-C__jkwvs.mjs} +22 -57
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +1 -1
- package/dist/dynamic/index.mjs +3 -3
- package/dist/{elevation-DtFxrG0s.mjs → elevation-DxQ6ACbt.mjs} +21 -7
- package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
- package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
- package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
- package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
- package/dist/events/index.d.mts +147 -36
- package/dist/events/index.mjs +338 -101
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +2 -2
- package/dist/{fields-DpZQa_Q3.d.mts → fields-BC7zcmI9.d.mts} +15 -3
- package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
- package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-q8oHt--L.mjs} +65 -7
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +29 -5
- package/dist/idempotency/index.mjs +111 -2
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-BLXBmWud.d.mts → index-C-xjcA6F.d.mts} +1 -1
- package/dist/{index-DtDzOBn8.d.mts → index-Cibkchnx.d.mts} +3 -134
- package/dist/{index-C1meYuDn.d.mts → index-CtGKT0lf.d.mts} +1 -1
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +9 -9
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +26 -8
- package/dist/integrations/mcp/index.mjs +96 -17
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +5 -0
- package/dist/integrations/webhooks.mjs +6 -0
- package/dist/{interface-CMRutPfe.d.mts → interface-YrWsmKqE.d.mts} +287 -179
- package/dist/{openapi-CbKUJY_m.mjs → openapi-CXuTG1M9.mjs} +2 -2
- package/dist/org/index.d.mts +1 -1
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-CH4cNwJi.mjs → permissions-oNZawnkR.mjs} +1 -1
- package/dist/plugins/index.d.mts +7 -7
- package/dist/plugins/index.mjs +11 -11
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +25 -32
- package/dist/presets/filesUpload.d.mts +26 -4
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +3 -2
- package/dist/presets/index.mjs +4 -3
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +91 -0
- package/dist/presets/search.mjs +150 -0
- package/dist/{presets-C2xgzW6x.mjs → presets-hM4WhNWY.mjs} +1 -1
- package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
- package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
- package/dist/{redis-BM00zaPB.d.mts → redis-MXLp1oOf.d.mts} +1 -1
- package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-C3cWymnW.mjs} +64 -47
- package/dist/rpc/index.d.mts +1 -1
- package/dist/rpc/index.mjs +1 -1
- package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
- package/dist/scope/index.mjs +1 -1
- package/dist/{sse-Ad7ypl9e.mjs → sse-CJpt7LGI.mjs} +1 -1
- package/dist/store-helpers-DFiZl5TL.mjs +57 -0
- package/dist/testing/index.d.mts +5 -14
- package/dist/testing/index.mjs +21 -75
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +2 -2
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BsbNMEDR.d.mts → types-CoSzA-s-.d.mts} +1 -1
- package/dist/{types-Ch9pTQbf.d.mts → types-CunEX4UX.d.mts} +10 -8
- package/dist/utils/index.d.mts +4 -4
- package/dist/utils/index.mjs +6 -6
- package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
- package/package.json +8 -11
- package/skills/arc/SKILL.md +92 -14
- package/skills/arc/references/auth.md +94 -0
- package/skills/arc/references/events.md +200 -12
- package/skills/arc/references/mcp.md +4 -17
- package/skills/arc/references/multi-tenancy.md +43 -0
- package/skills/arc/references/production.md +34 -19
- package/dist/EventTransport-BXja8NOc.d.mts +0 -135
- package/dist/audit/mongodb.d.mts +0 -2
- package/dist/audit/mongodb.mjs +0 -2
- package/dist/idempotency/mongodb.d.mts +0 -2
- package/dist/idempotency/mongodb.mjs +0 -123
- package/dist/mongodb-BsP-WbhN.d.mts +0 -127
- package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
- package/dist/mongodb-Utc5k_-0.mjs +0 -90
- /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
- /package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-Dq3_zBQP.mjs} +0 -0
- /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
- /package/dist/{caching-IMuYVjTL.mjs → caching-CjybdRwx.mjs} +0 -0
- /package/dist/{circuitBreaker-dTtG-UyS.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
- /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
- /package/dist/{errors-Ck2h67pm.d.mts → errors-BI8kEKsO.d.mts} +0 -0
- /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
- /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-B-pe8fhj.d.mts} +0 -0
- /package/dist/{interface-4y979v99.d.mts → interface-DplgQO2e.d.mts} +0 -0
- /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-Bksk8ydA.mjs} +0 -0
- /package/dist/{logger-D1YrIImS.mjs → logger-CDjpjySd.mjs} +0 -0
- /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
- /package/dist/{metrics-B-PU4-Yu.mjs → metrics-TuOmguhi.mjs} +0 -0
- /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
- /package/dist/{registry-BiTKT1Dg.mjs → registry-B0Wl7uVV.mjs} +0 -0
- /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
- /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
- /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-BwGQXUpd.d.mts} +0 -0
- /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
- /package/dist/{versioning-CDugduqI.mjs → versioning-Cm8qoFDg.mjs} +0 -0
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
148
|
-
import {
|
|
149
|
-
|
|
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
|
-
|
|
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
|
|
673
|
+
**In a custom route with Arc auth:**
|
|
659
674
|
|
|
660
675
|
```typescript
|
|
661
676
|
defineResource({
|
|
662
677
|
name: 'order',
|
|
663
|
-
|
|
678
|
+
routes: [{
|
|
664
679
|
method: 'POST',
|
|
665
680
|
path: '/:id/checkout',
|
|
666
681
|
permissions: requireAuth(),
|
|
667
|
-
|
|
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 };
|
package/dist/audit/mongodb.d.mts
DELETED
package/dist/audit/mongodb.mjs
DELETED
|
@@ -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,127 +0,0 @@
|
|
|
1
|
-
import { i as UserBase } from "./types-DZi1aYhm.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/audit/stores/interface.d.ts
|
|
4
|
-
type AuditAction = "create" | "update" | "delete" | "restore" | "custom";
|
|
5
|
-
interface AuditEntry {
|
|
6
|
-
/** Unique audit log ID */
|
|
7
|
-
id: string;
|
|
8
|
-
/** Resource name (e.g., 'product', 'user') */
|
|
9
|
-
resource: string;
|
|
10
|
-
/** Document/entity ID */
|
|
11
|
-
documentId: string;
|
|
12
|
-
/** Action performed */
|
|
13
|
-
action: AuditAction;
|
|
14
|
-
/** User who performed the action */
|
|
15
|
-
userId?: string;
|
|
16
|
-
/** Organization context */
|
|
17
|
-
organizationId?: string;
|
|
18
|
-
/** Previous state (for updates) */
|
|
19
|
-
before?: Record<string, unknown>;
|
|
20
|
-
/** New state (for creates/updates) */
|
|
21
|
-
after?: Record<string, unknown>;
|
|
22
|
-
/** Changed fields (for updates) */
|
|
23
|
-
changes?: string[];
|
|
24
|
-
/** Request ID for tracing */
|
|
25
|
-
requestId?: string;
|
|
26
|
-
/** IP address */
|
|
27
|
-
ipAddress?: string;
|
|
28
|
-
/** User agent */
|
|
29
|
-
userAgent?: string;
|
|
30
|
-
/** Custom metadata */
|
|
31
|
-
metadata?: Record<string, unknown>;
|
|
32
|
-
/** When the action occurred */
|
|
33
|
-
timestamp: Date;
|
|
34
|
-
}
|
|
35
|
-
interface AuditContext {
|
|
36
|
-
user?: UserBase;
|
|
37
|
-
organizationId?: string;
|
|
38
|
-
requestId?: string;
|
|
39
|
-
ipAddress?: string;
|
|
40
|
-
userAgent?: string;
|
|
41
|
-
/** HTTP method + route pattern (e.g., 'PATCH /api/products/:id') */
|
|
42
|
-
endpoint?: string;
|
|
43
|
-
/** Request duration in milliseconds */
|
|
44
|
-
duration?: number;
|
|
45
|
-
}
|
|
46
|
-
interface AuditStoreOptions {
|
|
47
|
-
/** Store name for logging */
|
|
48
|
-
name: string;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Abstract audit store interface
|
|
52
|
-
*/
|
|
53
|
-
interface AuditStore {
|
|
54
|
-
/** Store name */
|
|
55
|
-
readonly name: string;
|
|
56
|
-
/** Log an audit entry */
|
|
57
|
-
log(entry: AuditEntry): Promise<void>;
|
|
58
|
-
/** Query audit logs (optional - not all stores support querying) */
|
|
59
|
-
query?(options: AuditQueryOptions): Promise<AuditEntry[]>;
|
|
60
|
-
/** Close/cleanup (optional) */
|
|
61
|
-
close?(): Promise<void>;
|
|
62
|
-
}
|
|
63
|
-
interface AuditQueryOptions {
|
|
64
|
-
resource?: string;
|
|
65
|
-
documentId?: string;
|
|
66
|
-
userId?: string;
|
|
67
|
-
organizationId?: string;
|
|
68
|
-
action?: AuditAction | AuditAction[];
|
|
69
|
-
from?: Date;
|
|
70
|
-
to?: Date;
|
|
71
|
-
limit?: number;
|
|
72
|
-
offset?: number;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Create audit entry from context
|
|
76
|
-
*/
|
|
77
|
-
declare function createAuditEntry(resource: string, documentId: string, action: AuditAction, context: AuditContext, data?: {
|
|
78
|
-
before?: Record<string, unknown>;
|
|
79
|
-
after?: Record<string, unknown>;
|
|
80
|
-
metadata?: Record<string, unknown>;
|
|
81
|
-
}): AuditEntry;
|
|
82
|
-
//#endregion
|
|
83
|
-
//#region src/audit/stores/mongodb.d.ts
|
|
84
|
-
interface MongoAuditStoreOptions {
|
|
85
|
-
/** MongoDB connection or mongoose instance */
|
|
86
|
-
connection: MongoConnection;
|
|
87
|
-
/** Collection name (default: 'audit_logs') */
|
|
88
|
-
collection?: string;
|
|
89
|
-
/** TTL in days (default: 90, 0 = no expiry) */
|
|
90
|
-
ttlDays?: number;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Minimal MongoDB connection interface — DB-agnostic.
|
|
94
|
-
*
|
|
95
|
-
* Accepts:
|
|
96
|
-
* - Native MongoDB `Db` instance (has `.collection()`)
|
|
97
|
-
* - Mongoose `connection.db` (has `.collection()`)
|
|
98
|
-
* - Any object with `.collection(name)` method
|
|
99
|
-
*
|
|
100
|
-
* For Mongoose users: pass `mongoose.connection.db`, not `mongoose.connection`.
|
|
101
|
-
*/
|
|
102
|
-
interface MongoConnection {
|
|
103
|
-
collection: (name: string) => MongoCollection;
|
|
104
|
-
}
|
|
105
|
-
interface MongoCollection {
|
|
106
|
-
insertOne: (doc: Record<string, unknown>) => Promise<unknown>;
|
|
107
|
-
find: (query: Record<string, unknown>) => MongoCursor;
|
|
108
|
-
createIndex: (spec: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>;
|
|
109
|
-
}
|
|
110
|
-
interface MongoCursor {
|
|
111
|
-
sort: (spec: Record<string, unknown>) => MongoCursor;
|
|
112
|
-
skip: (n: number) => MongoCursor;
|
|
113
|
-
limit: (n: number) => MongoCursor;
|
|
114
|
-
toArray: () => Promise<Record<string, unknown>[]>;
|
|
115
|
-
}
|
|
116
|
-
declare class MongoAuditStore implements AuditStore {
|
|
117
|
-
readonly name = "mongodb";
|
|
118
|
-
private collection;
|
|
119
|
-
private initialized;
|
|
120
|
-
private ttlDays;
|
|
121
|
-
constructor(options: MongoAuditStoreOptions);
|
|
122
|
-
private ensureIndexes;
|
|
123
|
-
log(entry: AuditEntry): Promise<void>;
|
|
124
|
-
query(options?: AuditQueryOptions): Promise<AuditEntry[]>;
|
|
125
|
-
}
|
|
126
|
-
//#endregion
|
|
127
|
-
export { AuditContext as a, AuditStore as c, AuditAction as i, AuditStoreOptions as l, MongoAuditStoreOptions as n, AuditEntry as o, MongoConnection as r, AuditQueryOptions as s, MongoAuditStore as t, createAuditEntry as u };
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { n as IdempotencyResult, r as IdempotencyStore } from "./interface-DfLGcus7.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/idempotency/stores/mongodb.d.ts
|
|
4
|
-
interface MongoConnection {
|
|
5
|
-
db: {
|
|
6
|
-
collection(name: string): MongoCollection;
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
interface MongoCollection {
|
|
10
|
-
findOne(filter: object): Promise<IdempotencyDocument | null>;
|
|
11
|
-
insertOne(doc: object): Promise<{
|
|
12
|
-
acknowledged: boolean;
|
|
13
|
-
}>;
|
|
14
|
-
updateOne(filter: object, update: object, options?: object): Promise<{
|
|
15
|
-
acknowledged: boolean;
|
|
16
|
-
matchedCount: number;
|
|
17
|
-
modifiedCount: number;
|
|
18
|
-
upsertedCount?: number;
|
|
19
|
-
}>;
|
|
20
|
-
deleteOne(filter: object): Promise<{
|
|
21
|
-
deletedCount: number;
|
|
22
|
-
}>;
|
|
23
|
-
deleteMany(filter: object): Promise<{
|
|
24
|
-
deletedCount: number;
|
|
25
|
-
}>;
|
|
26
|
-
createIndex(spec: object, options?: object): Promise<string>;
|
|
27
|
-
}
|
|
28
|
-
interface IdempotencyDocument {
|
|
29
|
-
_id: string;
|
|
30
|
-
result?: {
|
|
31
|
-
statusCode: number;
|
|
32
|
-
headers: Record<string, string>;
|
|
33
|
-
body: unknown;
|
|
34
|
-
};
|
|
35
|
-
lock?: {
|
|
36
|
-
requestId: string;
|
|
37
|
-
expiresAt: Date;
|
|
38
|
-
};
|
|
39
|
-
createdAt: Date;
|
|
40
|
-
expiresAt: Date;
|
|
41
|
-
}
|
|
42
|
-
/** Minimal logger interface — compatible with console, pino, fastify.log */
|
|
43
|
-
interface IdempotencyLogger {
|
|
44
|
-
warn(message: string, ...args: unknown[]): void;
|
|
45
|
-
}
|
|
46
|
-
interface MongoIdempotencyStoreOptions {
|
|
47
|
-
/** Mongoose connection or MongoDB connection object */
|
|
48
|
-
connection: MongoConnection;
|
|
49
|
-
/** Collection name (default: 'arc_idempotency') */
|
|
50
|
-
collection?: string;
|
|
51
|
-
/** Create TTL index on startup (default: true) */
|
|
52
|
-
createIndex?: boolean;
|
|
53
|
-
/** Default TTL in ms (default: 86400000 = 24 hours) */
|
|
54
|
-
ttlMs?: number;
|
|
55
|
-
/** Logger for operational warnings (default: console) */
|
|
56
|
-
logger?: IdempotencyLogger;
|
|
57
|
-
}
|
|
58
|
-
declare class MongoIdempotencyStore implements IdempotencyStore {
|
|
59
|
-
readonly name = "mongodb";
|
|
60
|
-
private connection;
|
|
61
|
-
private collectionName;
|
|
62
|
-
private ttlMs;
|
|
63
|
-
private indexCreated;
|
|
64
|
-
private shouldEnsureIndex;
|
|
65
|
-
private logger;
|
|
66
|
-
constructor(options: MongoIdempotencyStoreOptions);
|
|
67
|
-
private get collection();
|
|
68
|
-
private ensureIndex;
|
|
69
|
-
get(key: string): Promise<IdempotencyResult | undefined>;
|
|
70
|
-
set(key: string, result: Omit<IdempotencyResult, "key">): Promise<void>;
|
|
71
|
-
tryLock(key: string, requestId: string, ttlMs: number): Promise<boolean>;
|
|
72
|
-
unlock(key: string, requestId: string): Promise<void>;
|
|
73
|
-
isLocked(key: string): Promise<boolean>;
|
|
74
|
-
delete(key: string): Promise<void>;
|
|
75
|
-
deleteByPrefix(prefix: string): Promise<number>;
|
|
76
|
-
findByPrefix(prefix: string): Promise<IdempotencyResult | undefined>;
|
|
77
|
-
close(): Promise<void>;
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
|
-
export { MongoIdempotencyStoreOptions as n, MongoIdempotencyStore as t };
|