@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.
- package/README.md +50 -38
- package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-CbKKIflT.mjs} +193 -143
- package/dist/EventTransport-CUw5NNWe.d.mts +293 -0
- package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-BPd6NQDm.mjs} +1 -1
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
- package/dist/audit/index.d.mts +135 -11
- package/dist/audit/index.mjs +107 -20
- package/dist/auth/index.d.mts +17 -9
- package/dist/auth/index.mjs +14 -7
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi-BBRVhjQN.mjs} +1 -1
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +15 -14
- package/dist/{caching-IMuYVjTL.mjs → caching-CBpK_SCM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -6
- package/dist/{defineResource-tcgySDo1.mjs → core-CcR01lup.mjs} +58 -61
- package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-Bp_5c_2b.mjs} +3 -3
- package/dist/{createApp-B1EY8zxa.mjs → createApp-BuvPma24.mjs} +15 -14
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DtFxrG0s.mjs → elevation-C7hgL_aI.mjs} +22 -8
- package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-Bb49BvPD.mjs} +59 -7
- package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DRQ3EqfL.d.mts} +37 -2
- package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-CxWgpd6K.d.mts} +14 -2
- package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-DCUjuiQT.mjs} +83 -5
- package/dist/events/index.d.mts +150 -36
- package/dist/events/index.mjs +355 -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/{types-DZi1aYhm.d.mts → fields-Lo1VUDpt.d.mts} +121 -1
- package/dist/{fields-ipsbIRPK.mjs → fields-bxkeltzz.mjs} +18 -5
- package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-t21LS-py.mjs} +65 -7
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +32 -5
- package/dist/idempotency/index.mjs +119 -12
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-DtDzOBn8.d.mts → index-8qw4y6ff.d.mts} +4 -135
- package/dist/{index-BLXBmWud.d.mts → index-ChIw3776.d.mts} +283 -408
- package/dist/{interface-CMRutPfe.d.mts → index-Cl0uoKd5.d.mts} +1758 -2506
- package/dist/{index-C1meYuDn.d.mts → index-DStwgFUK.d.mts} +81 -7
- package/dist/index.d.mts +7 -8
- package/dist/index.mjs +11 -12
- 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-D218ikEo.d.mts +77 -0
- package/dist/{memory-Cp7_cAko.mjs → memory-B5Amv9A1.mjs} +23 -8
- package/dist/{openapi-CbKUJY_m.mjs → openapi-B5F8AddX.mjs} +3 -3
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -4
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-CH4cNwJi.mjs → permissions-Dk6mshja.mjs} +315 -397
- package/dist/plugins/index.d.mts +7 -7
- package/dist/plugins/index.mjs +14 -16
- package/dist/plugins/response-cache.mjs +2 -2
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +27 -5
- 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 +2 -2
- package/dist/presets/search.d.mts +178 -0
- package/dist/presets/search.mjs +150 -0
- package/dist/{presets-C2xgzW6x.mjs → presets-fLJVXdVn.mjs} +1 -1
- package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-BKbWjgDG.d.mts} +1 -1
- package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DQCEfJis.mjs} +9 -9
- package/dist/{queryParser-CgCtsjti.mjs → queryParser-DBqBB6AC.mjs} +1 -1
- package/dist/{redis-BM00zaPB.d.mts → redis-DqyeggCa.d.mts} +1 -1
- package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-CakIQmwR.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-BElv3xPT.mjs} +65 -48
- package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
- package/dist/scope/index.d.mts +1 -1
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-Ad7ypl9e.mjs → sse-yBCgOLGu.mjs} +1 -1
- package/dist/store-helpers-ZCSMJJAX.mjs +57 -0
- package/dist/testing/index.d.mts +9 -17
- package/dist/testing/index.mjs +27 -83
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -31
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-BsbNMEDR.d.mts → types-Btdda02s.d.mts} +1 -1
- package/dist/{types-Ch9pTQbf.d.mts → types-Co8k3NyS.d.mts} +11 -9
- package/dist/types-Csi3FLfq.mjs +27 -0
- package/dist/utils/index.d.mts +208 -4
- package/dist/utils/index.mjs +5 -6
- package/dist/{utils-yYT3HDXt.mjs → utils-B2fNOD_i.mjs} +285 -2
- package/dist/{versioning-CDugduqI.mjs → versioning-C2U_bLY0.mjs} +3 -5
- package/package.json +20 -26
- package/skills/arc/SKILL.md +97 -23
- 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 -60
- 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/circuitBreaker-cmi5XDv5.mjs +0 -284
- package/dist/circuitBreaker-dTtG-UyS.d.mts +0 -206
- package/dist/core-F0QoWBt2.mjs +0 -34
- package/dist/dynamic/index.d.mts +0 -93
- package/dist/dynamic/index.mjs +0 -122
- package/dist/fields-DpZQa_Q3.d.mts +0 -109
- package/dist/idempotency/mongodb.d.mts +0 -2
- package/dist/idempotency/mongodb.mjs +0 -123
- package/dist/interface-4y979v99.d.mts +0 -54
- 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/policies/index.d.mts +0 -432
- package/dist/policies/index.mjs +0 -318
- package/dist/rpc/index.d.mts +0 -90
- package/dist/rpc/index.mjs +0 -248
- /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BNYKnrXF.mjs} +0 -0
- /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-C5SwtkAn.d.mts} +0 -0
- /package/dist/{errors-Ck2h67pm.d.mts → errors-CCSsMpXE.d.mts} +0 -0
- /package/dist/{errors-BF2bIOIS.mjs → errors-D5c-5BJL.mjs} +0 -0
- /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-BQ8QijNH.d.mts} +0 -0
- /package/dist/{interface-DfLGcus7.d.mts → interface-CSbZdv_3.d.mts} +0 -0
- /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-BAzJItAJ.mjs} +0 -0
- /package/dist/{logger-D1YrIImS.mjs → logger-DLg8-Ueg.mjs} +0 -0
- /package/dist/{metrics-B-PU4-Yu.mjs → metrics-DuhiSEZI.mjs} +0 -0
- /package/dist/{pluralize-CWP6MB39.mjs → pluralize-A0tWEl1K.mjs} +0 -0
- /package/dist/{registry-BiTKT1Dg.mjs → registry-B3lRFBWo.mjs} +0 -0
- /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-CXtJDAZ0.mjs} +0 -0
- /package/dist/{requestContext-DYvHl113.mjs → requestContext-xHIKedG6.mjs} +0 -0
- /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-BkzVU8h2.d.mts} +0 -0
- /package/dist/{storage-Dfzt4VTl.d.mts → storage-CVk_SEn2.d.mts} +0 -0
- /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-65B51Dw3.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
- /package/dist/{types-ZUu_h0jp.mjs → types-DV9WDfeg.mjs} +0 -0
package/dist/events/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as MemoryEventTransport, i as withRetry, o as
|
|
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.
|
|
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
|
-
|
|
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),
|
|
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-
|
|
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
|
|
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 {
|
package/dist/factory/index.d.mts
CHANGED
|
@@ -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-
|
|
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
|
package/dist/factory/index.mjs
CHANGED
|
@@ -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-
|
|
2
|
-
import { t as loadResources } from "../loadResources-
|
|
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
|
-
|
|
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 };
|