@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
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-Dl7MoVWH.mjs";
|
|
2
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DFiZl5TL.mjs";
|
|
2
3
|
//#region src/events/defineEvent.ts
|
|
3
4
|
/**
|
|
4
5
|
* defineEvent — Typed Event Definitions with Optional Schema Validation
|
|
@@ -223,6 +224,285 @@ const CACHE_EVENTS = Object.freeze({
|
|
|
223
224
|
TAG_VERSION_BUMPED: "arc.cache.tag.bumped"
|
|
224
225
|
});
|
|
225
226
|
//#endregion
|
|
227
|
+
//#region src/events/memory-outbox.ts
|
|
228
|
+
const DEFAULT_LEASE_MS$2 = 3e4;
|
|
229
|
+
var MemoryOutboxStore = class {
|
|
230
|
+
entries = [];
|
|
231
|
+
seenDedupeKeys = /* @__PURE__ */ new Set();
|
|
232
|
+
async save(event, options) {
|
|
233
|
+
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
234
|
+
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
235
|
+
if (options?.dedupeKey) {
|
|
236
|
+
if (this.seenDedupeKeys.has(options.dedupeKey)) return;
|
|
237
|
+
this.seenDedupeKeys.add(options.dedupeKey);
|
|
238
|
+
}
|
|
239
|
+
this.entries.push({
|
|
240
|
+
event,
|
|
241
|
+
status: "pending",
|
|
242
|
+
attempts: 0,
|
|
243
|
+
visibleAt: options?.visibleAt?.getTime() ?? 0,
|
|
244
|
+
leaseOwner: null,
|
|
245
|
+
leaseExpiresAt: 0,
|
|
246
|
+
deliveredAt: null,
|
|
247
|
+
firstFailedAt: null,
|
|
248
|
+
lastFailedAt: null,
|
|
249
|
+
lastError: null,
|
|
250
|
+
dedupeKey: options?.dedupeKey
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async getPending(limit) {
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
|
|
256
|
+
}
|
|
257
|
+
async claimPending(options) {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const limit = options?.limit ?? 100;
|
|
260
|
+
const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$2;
|
|
261
|
+
const consumerId = options?.consumerId ?? "anonymous";
|
|
262
|
+
const typeFilter = options?.types ? new Set(options.types) : null;
|
|
263
|
+
const claimed = [];
|
|
264
|
+
for (const entry of this.entries) {
|
|
265
|
+
if (claimed.length >= limit) break;
|
|
266
|
+
if (entry.status !== "pending") continue;
|
|
267
|
+
if (entry.visibleAt > now) continue;
|
|
268
|
+
if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
|
|
269
|
+
if (typeFilter && !typeFilter.has(entry.event.type)) continue;
|
|
270
|
+
entry.leaseOwner = consumerId;
|
|
271
|
+
entry.leaseExpiresAt = now + leaseMs;
|
|
272
|
+
entry.attempts++;
|
|
273
|
+
claimed.push(entry.event);
|
|
274
|
+
}
|
|
275
|
+
return claimed;
|
|
276
|
+
}
|
|
277
|
+
async acknowledge(eventId, options) {
|
|
278
|
+
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
279
|
+
if (!entry) return;
|
|
280
|
+
if (entry.status === "delivered") return;
|
|
281
|
+
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
282
|
+
entry.status = "delivered";
|
|
283
|
+
entry.deliveredAt = Date.now();
|
|
284
|
+
entry.leaseOwner = null;
|
|
285
|
+
}
|
|
286
|
+
async fail(eventId, error, options) {
|
|
287
|
+
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
288
|
+
if (!entry) return;
|
|
289
|
+
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
290
|
+
const now = Date.now();
|
|
291
|
+
entry.lastError = error;
|
|
292
|
+
entry.leaseOwner = null;
|
|
293
|
+
entry.leaseExpiresAt = 0;
|
|
294
|
+
if (entry.firstFailedAt === null) entry.firstFailedAt = now;
|
|
295
|
+
entry.lastFailedAt = now;
|
|
296
|
+
if (options?.deadLetter) {
|
|
297
|
+
entry.status = "dead_letter";
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
entry.status = "pending";
|
|
301
|
+
entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
|
|
302
|
+
}
|
|
303
|
+
async getDeadLettered(limit) {
|
|
304
|
+
const out = [];
|
|
305
|
+
for (const entry of this.entries) {
|
|
306
|
+
if (out.length >= limit) break;
|
|
307
|
+
if (entry.status !== "dead_letter") continue;
|
|
308
|
+
out.push({
|
|
309
|
+
event: entry.event,
|
|
310
|
+
error: {
|
|
311
|
+
message: entry.lastError?.message ?? "unknown",
|
|
312
|
+
...entry.lastError?.code !== void 0 ? { code: entry.lastError.code } : {}
|
|
313
|
+
},
|
|
314
|
+
attempts: entry.attempts,
|
|
315
|
+
firstFailedAt: new Date(entry.firstFailedAt ?? entry.lastFailedAt ?? Date.now()),
|
|
316
|
+
lastFailedAt: new Date(entry.lastFailedAt ?? entry.firstFailedAt ?? Date.now())
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
async purge(olderThanMs) {
|
|
322
|
+
const cutoff = Date.now() - olderThanMs;
|
|
323
|
+
let purged = 0;
|
|
324
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
325
|
+
const entry = this.entries[i];
|
|
326
|
+
if (!entry) continue;
|
|
327
|
+
if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
|
|
328
|
+
if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
|
|
329
|
+
this.entries.splice(i, 1);
|
|
330
|
+
purged++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return purged;
|
|
334
|
+
}
|
|
335
|
+
/** Test helper: inspect entry by id */
|
|
336
|
+
_getEntry(eventId) {
|
|
337
|
+
return this.entries.find((e) => e.event.meta.id === eventId);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/events/repository-outbox-adapter.ts
|
|
342
|
+
const DEFAULT_LEASE_MS$1 = 3e4;
|
|
343
|
+
const DEFAULT_CLAIM_LIMIT = 100;
|
|
344
|
+
const DEFAULT_PURGE_BATCH = 500;
|
|
345
|
+
function repositoryAsOutboxStore(repository) {
|
|
346
|
+
const missing = [];
|
|
347
|
+
if (typeof repository.create !== "function") missing.push("create");
|
|
348
|
+
if (typeof repository.getOne !== "function") missing.push("getOne");
|
|
349
|
+
if (typeof repository.findAll !== "function") missing.push("findAll");
|
|
350
|
+
if (typeof repository.deleteMany !== "function") missing.push("deleteMany");
|
|
351
|
+
if (typeof repository.findOneAndUpdate !== "function") missing.push("findOneAndUpdate");
|
|
352
|
+
if (missing.length > 0) throw new Error(`EventOutbox: repository is missing required methods: ${missing.join(", ")}. mongokit ≥3.8 satisfies all five; other kits must implement them to back the outbox.`);
|
|
353
|
+
const r = repository;
|
|
354
|
+
const isDuplicateKeyError = createIsDuplicateKeyError(repository);
|
|
355
|
+
const safeGetOne = createSafeGetOne(repository);
|
|
356
|
+
const isWellFormed = (event) => !!event && typeof event.type === "string" && !!event.meta?.id;
|
|
357
|
+
return {
|
|
358
|
+
async save(event, options) {
|
|
359
|
+
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
360
|
+
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
361
|
+
const now = /* @__PURE__ */ new Date();
|
|
362
|
+
const doc = {
|
|
363
|
+
_id: event.meta.id,
|
|
364
|
+
event,
|
|
365
|
+
type: event.type,
|
|
366
|
+
status: "pending",
|
|
367
|
+
attempts: 0,
|
|
368
|
+
visibleAt: options?.visibleAt ?? now,
|
|
369
|
+
leaseOwner: null,
|
|
370
|
+
leaseExpiresAt: null,
|
|
371
|
+
deliveredAt: null,
|
|
372
|
+
firstFailedAt: null,
|
|
373
|
+
lastFailedAt: null,
|
|
374
|
+
lastError: null,
|
|
375
|
+
dedupeKey: options?.dedupeKey ?? null,
|
|
376
|
+
partitionKey: options?.partitionKey ?? null,
|
|
377
|
+
headers: options?.headers ? { ...options.headers } : null,
|
|
378
|
+
createdAt: now
|
|
379
|
+
};
|
|
380
|
+
try {
|
|
381
|
+
await r.create(doc, options?.session ? { session: options.session } : void 0);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (isDuplicateKeyError(err)) return;
|
|
384
|
+
throw err;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
async getPending(limit) {
|
|
388
|
+
const now = /* @__PURE__ */ new Date();
|
|
389
|
+
return (await r.findAll({
|
|
390
|
+
status: "pending",
|
|
391
|
+
visibleAt: { $lte: now },
|
|
392
|
+
$or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }]
|
|
393
|
+
}, {
|
|
394
|
+
sort: { createdAt: 1 },
|
|
395
|
+
limit
|
|
396
|
+
})).map((d) => d.event).filter(isWellFormed);
|
|
397
|
+
},
|
|
398
|
+
async claimPending(options) {
|
|
399
|
+
const limit = options?.limit ?? DEFAULT_CLAIM_LIMIT;
|
|
400
|
+
const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS$1;
|
|
401
|
+
const consumerId = options?.consumerId ?? "anonymous";
|
|
402
|
+
const typeFilter = options?.types?.length ? { type: { $in: options.types } } : {};
|
|
403
|
+
const claimed = [];
|
|
404
|
+
for (let i = 0; i < limit; i++) {
|
|
405
|
+
const now = /* @__PURE__ */ new Date();
|
|
406
|
+
const leaseExpiresAt = new Date(now.getTime() + leaseMs);
|
|
407
|
+
const doc = await r.findOneAndUpdate({
|
|
408
|
+
status: "pending",
|
|
409
|
+
visibleAt: { $lte: now },
|
|
410
|
+
$or: [{ leaseOwner: null }, { leaseExpiresAt: { $lte: now } }],
|
|
411
|
+
...typeFilter
|
|
412
|
+
}, {
|
|
413
|
+
$set: {
|
|
414
|
+
leaseOwner: consumerId,
|
|
415
|
+
leaseExpiresAt
|
|
416
|
+
},
|
|
417
|
+
$inc: { attempts: 1 }
|
|
418
|
+
}, {
|
|
419
|
+
sort: { createdAt: 1 },
|
|
420
|
+
returnDocument: "after"
|
|
421
|
+
});
|
|
422
|
+
if (!doc) break;
|
|
423
|
+
if (isWellFormed(doc.event)) claimed.push(doc.event);
|
|
424
|
+
}
|
|
425
|
+
return claimed;
|
|
426
|
+
},
|
|
427
|
+
async acknowledge(eventId, options) {
|
|
428
|
+
const now = /* @__PURE__ */ new Date();
|
|
429
|
+
const filter = {
|
|
430
|
+
_id: eventId,
|
|
431
|
+
status: { $ne: "delivered" }
|
|
432
|
+
};
|
|
433
|
+
if (options?.consumerId) filter.leaseOwner = options.consumerId;
|
|
434
|
+
if (await r.findOneAndUpdate(filter, { $set: {
|
|
435
|
+
status: "delivered",
|
|
436
|
+
deliveredAt: now,
|
|
437
|
+
leaseOwner: null,
|
|
438
|
+
leaseExpiresAt: null
|
|
439
|
+
} }, { returnDocument: "after" })) return;
|
|
440
|
+
const current = await safeGetOne({ _id: eventId });
|
|
441
|
+
if (!current) return;
|
|
442
|
+
if (current.status === "delivered") return;
|
|
443
|
+
if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
|
|
444
|
+
},
|
|
445
|
+
async fail(eventId, error, options) {
|
|
446
|
+
const now = /* @__PURE__ */ new Date();
|
|
447
|
+
const targetStatus = options?.deadLetter ? "dead_letter" : "pending";
|
|
448
|
+
const visibleAt = options?.retryAt ?? now;
|
|
449
|
+
const filter = { _id: eventId };
|
|
450
|
+
if (options?.consumerId) filter.leaseOwner = options.consumerId;
|
|
451
|
+
const pipeline = [{ $set: {
|
|
452
|
+
status: targetStatus,
|
|
453
|
+
visibleAt,
|
|
454
|
+
leaseOwner: null,
|
|
455
|
+
leaseExpiresAt: null,
|
|
456
|
+
lastFailedAt: now,
|
|
457
|
+
lastError: {
|
|
458
|
+
message: error.message,
|
|
459
|
+
...error.code ? { code: error.code } : {}
|
|
460
|
+
},
|
|
461
|
+
firstFailedAt: { $ifNull: ["$firstFailedAt", now] }
|
|
462
|
+
} }];
|
|
463
|
+
if (await r.findOneAndUpdate(filter, pipeline, { returnDocument: "after" })) return;
|
|
464
|
+
const current = await safeGetOne({ _id: eventId });
|
|
465
|
+
if (!current) return;
|
|
466
|
+
if (options?.consumerId && current.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, current.leaseOwner);
|
|
467
|
+
},
|
|
468
|
+
async getDeadLettered(limit) {
|
|
469
|
+
return (await r.findAll({ status: "dead_letter" }, {
|
|
470
|
+
sort: { _id: 1 },
|
|
471
|
+
limit
|
|
472
|
+
})).filter((d) => isWellFormed(d.event)).map((d) => ({
|
|
473
|
+
event: d.event,
|
|
474
|
+
error: {
|
|
475
|
+
message: d.lastError?.message ?? "unknown",
|
|
476
|
+
...d.lastError?.code !== void 0 ? { code: d.lastError.code } : {}
|
|
477
|
+
},
|
|
478
|
+
attempts: d.attempts,
|
|
479
|
+
firstFailedAt: d.firstFailedAt ?? d.lastFailedAt ?? d.createdAt,
|
|
480
|
+
lastFailedAt: d.lastFailedAt ?? d.firstFailedAt ?? d.createdAt
|
|
481
|
+
}));
|
|
482
|
+
},
|
|
483
|
+
async purge(olderThanMs) {
|
|
484
|
+
const cutoff = new Date(Date.now() - olderThanMs);
|
|
485
|
+
let totalDeleted = 0;
|
|
486
|
+
for (;;) {
|
|
487
|
+
const batch = await r.findAll({
|
|
488
|
+
status: "delivered",
|
|
489
|
+
deliveredAt: { $lte: cutoff }
|
|
490
|
+
}, {
|
|
491
|
+
sort: { deliveredAt: 1 },
|
|
492
|
+
limit: DEFAULT_PURGE_BATCH,
|
|
493
|
+
select: "_id"
|
|
494
|
+
});
|
|
495
|
+
if (batch.length === 0) break;
|
|
496
|
+
const ids = batch.map((d) => d._id);
|
|
497
|
+
const res = await r.deleteMany({ _id: { $in: ids } });
|
|
498
|
+
totalDeleted += res.deletedCount ?? 0;
|
|
499
|
+
if (batch.length < DEFAULT_PURGE_BATCH) break;
|
|
500
|
+
}
|
|
501
|
+
return totalDeleted;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
//#endregion
|
|
226
506
|
//#region src/events/outbox.ts
|
|
227
507
|
/** Default outbox retention — delivered events older than this are eligible for purge */
|
|
228
508
|
const DEFAULT_OUTBOX_RETENTION_MS = 10080 * 60 * 1e3;
|
|
@@ -263,14 +543,25 @@ var EventOutbox = class {
|
|
|
263
543
|
_leaseMs;
|
|
264
544
|
_onError;
|
|
265
545
|
_usePublishMany;
|
|
546
|
+
_failurePolicy;
|
|
547
|
+
/**
|
|
548
|
+
* In-process attempt counter per event id. Accurate within this relay
|
|
549
|
+
* process; resets on restart. Populated as failures occur and cleared on
|
|
550
|
+
* successful ack or dead-letter transition. For durable authoritative
|
|
551
|
+
* counts, apps can query the store directly inside {@link OutboxFailurePolicy}.
|
|
552
|
+
*/
|
|
553
|
+
_attempts = /* @__PURE__ */ new Map();
|
|
266
554
|
constructor(opts) {
|
|
267
|
-
this._store = opts.
|
|
555
|
+
if (opts.repository) this._store = repositoryAsOutboxStore(opts.repository);
|
|
556
|
+
else if (opts.store) this._store = opts.store;
|
|
557
|
+
else throw new Error("EventOutbox: either `repository` or `store` must be provided. Pass a RepositoryLike (mongokit / prismakit) for the common case, or a concrete OutboxStore (memory / custom) for non-repository backends.");
|
|
268
558
|
this._transport = opts.transport;
|
|
269
559
|
this._batchSize = opts.batchSize ?? 100;
|
|
270
560
|
this._consumerId = opts.consumerId ?? `relay-${Math.random().toString(36).slice(2, 10)}`;
|
|
271
561
|
this._leaseMs = opts.leaseMs ?? DEFAULT_LEASE_MS;
|
|
272
562
|
this._onError = opts.onError;
|
|
273
563
|
this._usePublishMany = opts.usePublishMany ?? true;
|
|
564
|
+
this._failurePolicy = opts.failurePolicy;
|
|
274
565
|
}
|
|
275
566
|
/** Unique consumer ID used for lease ownership when the store supports claims */
|
|
276
567
|
get consumerId() {
|
|
@@ -292,7 +583,11 @@ var EventOutbox = class {
|
|
|
292
583
|
if (!event || typeof event !== "object") throw new InvalidOutboxEventError("event is not an object");
|
|
293
584
|
if (!event.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
294
585
|
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
295
|
-
|
|
586
|
+
const effectiveOptions = options?.dedupeKey === void 0 && event.meta.idempotencyKey ? {
|
|
587
|
+
...options ?? {},
|
|
588
|
+
dedupeKey: event.meta.idempotencyKey
|
|
589
|
+
} : options;
|
|
590
|
+
await this._store.save(event, effectiveOptions);
|
|
296
591
|
}
|
|
297
592
|
_reportError(kind, error, event) {
|
|
298
593
|
if (!this._onError) return;
|
|
@@ -357,6 +652,7 @@ var EventOutbox = class {
|
|
|
357
652
|
ownershipMismatches: 0,
|
|
358
653
|
malformed: 0,
|
|
359
654
|
failHookErrors: 0,
|
|
655
|
+
deadLettered: 0,
|
|
360
656
|
usedPublishMany: false
|
|
361
657
|
};
|
|
362
658
|
if (!this._transport) return empty;
|
|
@@ -380,7 +676,8 @@ var EventOutbox = class {
|
|
|
380
676
|
publishFailed: 0,
|
|
381
677
|
ackFailed: 0,
|
|
382
678
|
ownershipMismatches: 0,
|
|
383
|
-
failHookErrors: 0
|
|
679
|
+
failHookErrors: 0,
|
|
680
|
+
deadLettered: 0
|
|
384
681
|
};
|
|
385
682
|
const canPublishMany = this._usePublishMany && typeof this._transport.publishMany === "function";
|
|
386
683
|
const canFail = typeof this._store.fail === "function";
|
|
@@ -414,8 +711,28 @@ var EventOutbox = class {
|
|
|
414
711
|
stopBatch = true;
|
|
415
712
|
continue;
|
|
416
713
|
}
|
|
714
|
+
const attempts = (this._attempts.get(event.meta.id) ?? 0) + 1;
|
|
715
|
+
this._attempts.set(event.meta.id, attempts);
|
|
716
|
+
let failOpts = { consumerId: this._consumerId };
|
|
717
|
+
if (this._failurePolicy) try {
|
|
718
|
+
const decision = await this._failurePolicy({
|
|
719
|
+
event,
|
|
720
|
+
error: publishErr,
|
|
721
|
+
attempts
|
|
722
|
+
});
|
|
723
|
+
failOpts = {
|
|
724
|
+
...failOpts,
|
|
725
|
+
...decision
|
|
726
|
+
};
|
|
727
|
+
} catch (policyErr) {
|
|
728
|
+
this._reportError("fail_failed", policyErr, event);
|
|
729
|
+
}
|
|
417
730
|
try {
|
|
418
|
-
await this._store.fail(event.meta.id, normalizeError(publishErr),
|
|
731
|
+
await this._store.fail(event.meta.id, normalizeError(publishErr), failOpts);
|
|
732
|
+
if (failOpts.deadLetter) {
|
|
733
|
+
counts.deadLettered++;
|
|
734
|
+
this._attempts.delete(event.meta.id);
|
|
735
|
+
}
|
|
419
736
|
} catch (failErr) {
|
|
420
737
|
if (failErr instanceof OutboxOwnershipError) {
|
|
421
738
|
counts.ownershipMismatches++;
|
|
@@ -430,6 +747,7 @@ var EventOutbox = class {
|
|
|
430
747
|
try {
|
|
431
748
|
await this._store.acknowledge(event.meta.id, { consumerId: this._consumerId });
|
|
432
749
|
counts.relayed++;
|
|
750
|
+
this._attempts.delete(event.meta.id);
|
|
433
751
|
} catch (ackErr) {
|
|
434
752
|
counts.ackFailed++;
|
|
435
753
|
if (ackErr instanceof OutboxOwnershipError) {
|
|
@@ -446,10 +764,24 @@ var EventOutbox = class {
|
|
|
446
764
|
ownershipMismatches: counts.ownershipMismatches,
|
|
447
765
|
malformed,
|
|
448
766
|
failHookErrors: counts.failHookErrors,
|
|
767
|
+
deadLettered: counts.deadLettered,
|
|
449
768
|
usedPublishMany: canPublishMany && valid.length > 0
|
|
450
769
|
};
|
|
451
770
|
}
|
|
452
771
|
/**
|
|
772
|
+
* Fetch current dead-lettered events as typed {@link DeadLetteredEvent}
|
|
773
|
+
* envelopes. Delegates to {@link OutboxStore.getDeadLettered} — returns
|
|
774
|
+
* `[]` when the store doesn't implement it.
|
|
775
|
+
*
|
|
776
|
+
* Pairs with {@link OutboxFailurePolicy}: apps set a policy that routes to
|
|
777
|
+
* `deadLetter: true` after N attempts, then read back with this to alert,
|
|
778
|
+
* replay, or archive.
|
|
779
|
+
*/
|
|
780
|
+
async getDeadLettered(limit = 100) {
|
|
781
|
+
if (!this._store.getDeadLettered) return [];
|
|
782
|
+
return this._store.getDeadLettered(limit);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
453
785
|
* Purge old **delivered** events from the outbox store.
|
|
454
786
|
* Delegates to `store.purge()` if implemented; no-op otherwise.
|
|
455
787
|
* @param olderThanMs - Remove events delivered more than this many ms ago (default: 7 days)
|
|
@@ -468,101 +800,6 @@ function normalizeError(err) {
|
|
|
468
800
|
return { message: String(err) };
|
|
469
801
|
}
|
|
470
802
|
/**
|
|
471
|
-
* In-memory outbox store — reference implementation supporting the full
|
|
472
|
-
* capability set (claim/lease, fail/retry, dedupe, visibleAt).
|
|
473
|
-
*
|
|
474
|
-
* For dev/testing only. Production deployments should use a durable store
|
|
475
|
-
* backed by the app's database.
|
|
476
|
-
*/
|
|
477
|
-
var MemoryOutboxStore = class {
|
|
478
|
-
entries = [];
|
|
479
|
-
seenDedupeKeys = /* @__PURE__ */ new Set();
|
|
480
|
-
async save(event, options) {
|
|
481
|
-
if (!event?.type || typeof event.type !== "string") throw new InvalidOutboxEventError("event.type is required");
|
|
482
|
-
if (!event.meta?.id || typeof event.meta.id !== "string") throw new InvalidOutboxEventError("event.meta.id is required");
|
|
483
|
-
if (options?.dedupeKey) {
|
|
484
|
-
if (this.seenDedupeKeys.has(options.dedupeKey)) return;
|
|
485
|
-
this.seenDedupeKeys.add(options.dedupeKey);
|
|
486
|
-
}
|
|
487
|
-
this.entries.push({
|
|
488
|
-
event,
|
|
489
|
-
status: "pending",
|
|
490
|
-
attempts: 0,
|
|
491
|
-
visibleAt: options?.visibleAt?.getTime() ?? 0,
|
|
492
|
-
leaseOwner: null,
|
|
493
|
-
leaseExpiresAt: 0,
|
|
494
|
-
deliveredAt: null,
|
|
495
|
-
lastError: null,
|
|
496
|
-
dedupeKey: options?.dedupeKey
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
async getPending(limit) {
|
|
500
|
-
const now = Date.now();
|
|
501
|
-
return this.entries.filter((e) => e.status === "pending" && e.visibleAt <= now && (e.leaseOwner === null || e.leaseExpiresAt <= now)).slice(0, limit).map((e) => e.event);
|
|
502
|
-
}
|
|
503
|
-
async claimPending(options) {
|
|
504
|
-
const now = Date.now();
|
|
505
|
-
const limit = options?.limit ?? 100;
|
|
506
|
-
const leaseMs = options?.leaseMs ?? DEFAULT_LEASE_MS;
|
|
507
|
-
const consumerId = options?.consumerId ?? "anonymous";
|
|
508
|
-
const typeFilter = options?.types ? new Set(options.types) : null;
|
|
509
|
-
const claimed = [];
|
|
510
|
-
for (const entry of this.entries) {
|
|
511
|
-
if (claimed.length >= limit) break;
|
|
512
|
-
if (entry.status !== "pending") continue;
|
|
513
|
-
if (entry.visibleAt > now) continue;
|
|
514
|
-
if (entry.leaseOwner !== null && entry.leaseExpiresAt > now) continue;
|
|
515
|
-
if (typeFilter && !typeFilter.has(entry.event.type)) continue;
|
|
516
|
-
entry.leaseOwner = consumerId;
|
|
517
|
-
entry.leaseExpiresAt = now + leaseMs;
|
|
518
|
-
entry.attempts++;
|
|
519
|
-
claimed.push(entry.event);
|
|
520
|
-
}
|
|
521
|
-
return claimed;
|
|
522
|
-
}
|
|
523
|
-
async acknowledge(eventId, options) {
|
|
524
|
-
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
525
|
-
if (!entry) return;
|
|
526
|
-
if (entry.status === "delivered") return;
|
|
527
|
-
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
528
|
-
entry.status = "delivered";
|
|
529
|
-
entry.deliveredAt = Date.now();
|
|
530
|
-
entry.leaseOwner = null;
|
|
531
|
-
}
|
|
532
|
-
async fail(eventId, error, options) {
|
|
533
|
-
const entry = this.entries.find((e) => e.event.meta.id === eventId);
|
|
534
|
-
if (!entry) return;
|
|
535
|
-
if (options?.consumerId && entry.leaseOwner && entry.leaseOwner !== options.consumerId) throw new OutboxOwnershipError(eventId, options.consumerId, entry.leaseOwner);
|
|
536
|
-
entry.lastError = error;
|
|
537
|
-
entry.leaseOwner = null;
|
|
538
|
-
entry.leaseExpiresAt = 0;
|
|
539
|
-
if (options?.deadLetter) {
|
|
540
|
-
entry.status = "dead_letter";
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
entry.status = "pending";
|
|
544
|
-
entry.visibleAt = options?.retryAt ? options.retryAt.getTime() : 0;
|
|
545
|
-
}
|
|
546
|
-
async purge(olderThanMs) {
|
|
547
|
-
const cutoff = Date.now() - olderThanMs;
|
|
548
|
-
let purged = 0;
|
|
549
|
-
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
550
|
-
const entry = this.entries[i];
|
|
551
|
-
if (!entry) continue;
|
|
552
|
-
if (entry.status === "delivered" && entry.deliveredAt !== null && entry.deliveredAt < cutoff) {
|
|
553
|
-
if (entry.dedupeKey) this.seenDedupeKeys.delete(entry.dedupeKey);
|
|
554
|
-
this.entries.splice(i, 1);
|
|
555
|
-
purged++;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return purged;
|
|
559
|
-
}
|
|
560
|
-
/** Test helper: inspect entry by id */
|
|
561
|
-
_getEntry(eventId) {
|
|
562
|
-
return this.entries.find((e) => e.event.meta.id === eventId);
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
/**
|
|
566
803
|
* Compute a `retryAt` `Date` using exponential backoff with jitter.
|
|
567
804
|
*
|
|
568
805
|
* This is a convenience helper for store authors implementing
|
|
@@ -606,4 +843,4 @@ function exponentialBackoff(options) {
|
|
|
606
843
|
return new Date(now + jittered);
|
|
607
844
|
}
|
|
608
845
|
//#endregion
|
|
609
|
-
export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
|
|
846
|
+
export { ARC_LIFECYCLE_EVENTS, CACHE_EVENTS, CRUD_EVENT_SUFFIXES, EventOutbox, InvalidOutboxEventError, MemoryEventTransport, MemoryOutboxStore, OutboxOwnershipError, createChildEvent, createDeadLetterPublisher, createEvent, createEventRegistry, crudEventType, defineEvent, eventPlugin, exponentialBackoff, withRetry };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-
|
|
1
|
+
import { n as RedisStreamTransport, r as RedisStreamTransportOptions, t as RedisStreamLike } from "../../redis-stream-Bz-4q96t.mjs";
|
|
2
2
|
export { type RedisStreamLike, RedisStreamTransport, type RedisStreamTransportOptions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { i as EventLogger, n as DomainEvent, o as EventTransport, r as EventHandler } from "../../EventTransport-CqZ8FyM_.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/events/transports/redis.d.ts
|
|
4
4
|
interface RedisLike {
|
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-CunEX4UX.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-CBJUJKGP.mjs";
|
|
2
|
+
import { t as loadResources } from "../loadResources-Bksk8ydA.mjs";
|
|
3
3
|
//#region src/factory/edge.ts
|
|
4
4
|
/**
|
|
5
5
|
* Convert a Fastify app into a Web Standards fetch handler.
|
|
@@ -86,16 +86,28 @@ declare const fields: {
|
|
|
86
86
|
* @returns The filtered object
|
|
87
87
|
*/
|
|
88
88
|
declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
|
|
89
|
+
/**
|
|
90
|
+
* Result of applying write permissions — includes both the filtered body
|
|
91
|
+
* and the list of fields that were stripped so callers can decide whether
|
|
92
|
+
* to reject the request (secure default) or silently strip (legacy).
|
|
93
|
+
*/
|
|
94
|
+
interface FieldWritePermissionResult<T extends Record<string, unknown>> {
|
|
95
|
+
readonly body: T;
|
|
96
|
+
readonly deniedFields: readonly string[];
|
|
97
|
+
}
|
|
89
98
|
/**
|
|
90
99
|
* Apply field-level WRITE permissions to request body.
|
|
91
|
-
*
|
|
100
|
+
*
|
|
101
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
102
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
103
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
104
|
+
* `BodySanitizer` for the default policy.
|
|
92
105
|
*
|
|
93
106
|
* @param body - The request body (returns a new filtered copy)
|
|
94
107
|
* @param fieldPermissions - Field permission map from resource config
|
|
95
108
|
* @param userRoles - Current user's roles
|
|
96
|
-
* @returns Filtered body
|
|
97
109
|
*/
|
|
98
|
-
declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T
|
|
110
|
+
declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
|
|
99
111
|
/**
|
|
100
112
|
* Resolve effective roles by merging global user roles with org-level roles.
|
|
101
113
|
*
|
|
@@ -77,24 +77,37 @@ function applyFieldReadPermissions(data, fieldPermissions, userRoles) {
|
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
79
|
* Apply field-level WRITE permissions to request body.
|
|
80
|
-
*
|
|
80
|
+
*
|
|
81
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
82
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
83
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
84
|
+
* `BodySanitizer` for the default policy.
|
|
81
85
|
*
|
|
82
86
|
* @param body - The request body (returns a new filtered copy)
|
|
83
87
|
* @param fieldPermissions - Field permission map from resource config
|
|
84
88
|
* @param userRoles - Current user's roles
|
|
85
|
-
* @returns Filtered body
|
|
86
89
|
*/
|
|
87
90
|
function applyFieldWritePermissions(body, fieldPermissions, userRoles) {
|
|
88
91
|
const result = { ...body };
|
|
92
|
+
const deniedFields = [];
|
|
89
93
|
for (const [field, perm] of Object.entries(fieldPermissions)) switch (perm._type) {
|
|
90
94
|
case "hidden":
|
|
91
|
-
|
|
95
|
+
if (field in result) {
|
|
96
|
+
deniedFields.push(field);
|
|
97
|
+
delete result[field];
|
|
98
|
+
}
|
|
92
99
|
break;
|
|
93
100
|
case "writableBy":
|
|
94
|
-
if (field in result && !perm.roles?.some((r) => userRoles.includes(r)))
|
|
101
|
+
if (field in result && !perm.roles?.some((r) => userRoles.includes(r))) {
|
|
102
|
+
deniedFields.push(field);
|
|
103
|
+
delete result[field];
|
|
104
|
+
}
|
|
95
105
|
break;
|
|
96
106
|
}
|
|
97
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
body: result,
|
|
109
|
+
deniedFields
|
|
110
|
+
};
|
|
98
111
|
}
|
|
99
112
|
/**
|
|
100
113
|
* Resolve effective roles by merging global user roles with org-level roles.
|