@classytic/arc 2.18.5 → 2.20.0
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 +1 -1
- package/bin/arc.js +37 -0
- package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
- package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
- package/dist/{actionPermissions-h7DYDNMg.mjs → actionPermissions-C9RMiIpc.mjs} +2 -1
- package/dist/audit/index.mjs +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +17 -6
- package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
- package/dist/cache/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +2 -2
- 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 +4 -13
- package/dist/cli/index.mjs +2 -2
- package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +4 -4
- package/dist/{core-ZnQBa5rh.mjs → core-CWhyGkiM.mjs} +136 -59
- package/dist/{createActionRouter-CagItpxI.mjs → createActionRouter-HlQso0jO.mjs} +8 -7
- package/dist/{createAggregationRouter-DVKroXhC.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
- package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/encryption/index.d.mts +222 -0
- package/dist/encryption/index.mjs +270 -0
- package/dist/events/index.mjs +9 -6
- package/dist/factory/index.d.mts +3 -2
- package/dist/factory/index.mjs +2 -2
- package/dist/{fastify-augmentation-Cxb0ROIw.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
- package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +1 -1
- package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
- package/dist/{index-Dh0_RSTd.d.mts → index-DUa8WHz3.d.mts} +7 -7
- package/dist/{index-CHNQIhcb.d.mts → index-_--GyzcO.d.mts} +111 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +6 -6
- package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/streamline.d.mts +17 -5
- package/dist/integrations/streamline.mjs +9 -5
- package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
- package/dist/{mcpPlugin-D1wuRD2c.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/module-JYH9V0I8.d.mts +384 -0
- package/dist/{openapi-xVJibapr.mjs → openapi-Dq6RbEGK.mjs} +3 -3
- package/dist/permissions/index.d.mts +1 -1
- package/dist/permissions/index.mjs +1 -1
- package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +10 -5
- package/dist/plugins/index.mjs +3 -3
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +16 -2
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +3 -2
- package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
- package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
- package/dist/{registerRoute-CPmdkZGu.mjs → registerRoute-DGSREwPw.mjs} +20 -2
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
- package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
- package/dist/scim/index.mjs +2 -2
- package/dist/sync/changelog.d.mts +2 -0
- package/dist/sync/changelog.mjs +2 -0
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +2 -2
- package/dist/types/index.d.mts +2 -2
- package/dist/{types-CmkknLzI.d.mts → types-BHMKlMBU.d.mts} +53 -195
- package/dist/{types-D60PFwfG.d.mts → types-C9b2HR5W.d.mts} +1 -1
- package/dist/utils/index.d.mts +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
- package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
- package/package.json +22 -8
- package/skills/arc/SKILL.md +10 -0
- package/skills/arc/references/mcp.md +2 -0
|
@@ -245,7 +245,7 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
|
|
|
245
245
|
trackPlugin("arc-caching", opts);
|
|
246
246
|
}
|
|
247
247
|
if (config.arcPlugins?.queryCache) {
|
|
248
|
-
const { queryCachePlugin } = await import("./queryCachePlugin-
|
|
248
|
+
const { queryCachePlugin } = await import("./queryCachePlugin-DzqLN904.mjs").then((n) => n.n);
|
|
249
249
|
const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
|
|
250
250
|
const store = config.stores?.queryCache ?? new (await (import("./memory-CLXA2VeJ.mjs").then((n) => n.n))).MemoryCacheStore();
|
|
251
251
|
await fastify.register(queryCachePlugin, {
|
|
@@ -384,6 +384,134 @@ function createOptionalAuthenticate(authenticate) {
|
|
|
384
384
|
};
|
|
385
385
|
}
|
|
386
386
|
//#endregion
|
|
387
|
+
//#region src/factory/module.ts
|
|
388
|
+
/**
|
|
389
|
+
* Resolve any module-input form to a concrete `ArcModule`. A thunk is invoked
|
|
390
|
+
* (this is where a dynamic `import()` fires); a promise (or plain module) is
|
|
391
|
+
* awaited. Called once per module at the start of the bootstrap phase.
|
|
392
|
+
*/
|
|
393
|
+
async function resolveModule(input) {
|
|
394
|
+
const resolved = typeof input === "function" ? await input() : await input;
|
|
395
|
+
if (!resolved || typeof resolved !== "object" || typeof resolved.name !== "string") throw new Error("[arc] a `modules` entry resolved to something that is not an ArcModule (expected an object with a string `name`). Check dynamic-import thunks return `createXModule(deps)` (or the module object), not the namespace.");
|
|
396
|
+
return resolved;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Order modules for composition by their `dependsOn` edges — a STABLE
|
|
400
|
+
* topological sort. Called once at the start of the bootstrap phase; every
|
|
401
|
+
* subsequent phase (bootstrap, resources, afterResources, and reverse-order
|
|
402
|
+
* onClose) iterates the returned list, so a module's declared dependencies are
|
|
403
|
+
* always composed before it.
|
|
404
|
+
*
|
|
405
|
+
* "Stable" = modules with no edge between them keep their original list order,
|
|
406
|
+
* so declaring `dependsOn` on one module never silently reorders an unrelated
|
|
407
|
+
* one. Backward compatible: a `modules` array with NO `dependsOn` anywhere is
|
|
408
|
+
* returned unchanged.
|
|
409
|
+
*
|
|
410
|
+
* Fail-fast — throws (never reorders past a broken contract) on:
|
|
411
|
+
* - duplicate module names (the name is the graph key)
|
|
412
|
+
* - a `dependsOn` name not present in the composed set
|
|
413
|
+
* - a self-reference (`dependsOn` includes the module's own name)
|
|
414
|
+
* - a dependency cycle (reports the concrete `a → b → … → a` path)
|
|
415
|
+
*/
|
|
416
|
+
function orderModules(modules) {
|
|
417
|
+
const byName = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const m of modules) {
|
|
419
|
+
if (byName.has(m.name)) throw new Error(`[arc] Duplicate module name "${m.name}" — composed twice; check your modules array.`);
|
|
420
|
+
byName.set(m.name, m);
|
|
421
|
+
}
|
|
422
|
+
for (const m of modules) for (const dep of m.dependsOn ?? []) {
|
|
423
|
+
if (dep === m.name) throw new Error(`[arc] module "${m.name}" dependsOn itself — remove the self-reference.`);
|
|
424
|
+
if (!byName.has(dep)) throw new Error(`[arc] module "${m.name}" dependsOn "${dep}", which is not composed. Add the "${dep}" module to createApp({ modules }) (before this one is fine — arc orders them), or drop the dependency. Composed modules: ${[...byName.keys()].join(", ") || "(none)"}.`);
|
|
425
|
+
}
|
|
426
|
+
if (modules.every((m) => !m.dependsOn || m.dependsOn.length === 0)) return [...modules];
|
|
427
|
+
const originalIndex = /* @__PURE__ */ new Map();
|
|
428
|
+
let nextIndex = 0;
|
|
429
|
+
for (const m of modules) originalIndex.set(m.name, nextIndex++);
|
|
430
|
+
const pendingDeps = new Map(modules.map((m) => [m.name, (m.dependsOn ?? []).length]));
|
|
431
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const m of modules) for (const dep of m.dependsOn ?? []) {
|
|
433
|
+
const list = dependents.get(dep);
|
|
434
|
+
if (list) list.push(m.name);
|
|
435
|
+
else dependents.set(dep, [m.name]);
|
|
436
|
+
}
|
|
437
|
+
const ordered = [];
|
|
438
|
+
const remaining = new Set(modules.map((m) => m.name));
|
|
439
|
+
while (remaining.size > 0) {
|
|
440
|
+
let pick;
|
|
441
|
+
let pickIndex = Number.POSITIVE_INFINITY;
|
|
442
|
+
for (const name of remaining) {
|
|
443
|
+
if (pendingDeps.get(name) !== 0) continue;
|
|
444
|
+
const index = originalIndex.get(name) ?? Number.POSITIVE_INFINITY;
|
|
445
|
+
if (index < pickIndex) {
|
|
446
|
+
pick = name;
|
|
447
|
+
pickIndex = index;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (pick === void 0) throw new Error(describeModuleCycle(remaining, byName));
|
|
451
|
+
const picked = byName.get(pick);
|
|
452
|
+
if (!picked) throw new Error(`[arc] internal: ordered module "${pick}" is missing from the index`);
|
|
453
|
+
ordered.push(picked);
|
|
454
|
+
remaining.delete(pick);
|
|
455
|
+
for (const dependent of dependents.get(pick) ?? []) pendingDeps.set(dependent, (pendingDeps.get(dependent) ?? 1) - 1);
|
|
456
|
+
}
|
|
457
|
+
return ordered;
|
|
458
|
+
}
|
|
459
|
+
/** Walk the still-unordered subgraph for one concrete cycle path. */
|
|
460
|
+
function describeModuleCycle(remaining, byName) {
|
|
461
|
+
const stack = [];
|
|
462
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
463
|
+
const done = /* @__PURE__ */ new Set();
|
|
464
|
+
let cycle = null;
|
|
465
|
+
const walk = (name) => {
|
|
466
|
+
if (cycle) return;
|
|
467
|
+
stack.push(name);
|
|
468
|
+
onStack.add(name);
|
|
469
|
+
for (const dep of byName.get(name)?.dependsOn ?? []) {
|
|
470
|
+
if (!remaining.has(dep)) continue;
|
|
471
|
+
if (onStack.has(dep)) {
|
|
472
|
+
cycle = [...stack.slice(stack.indexOf(dep)), dep];
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (!done.has(dep)) walk(dep);
|
|
476
|
+
if (cycle) return;
|
|
477
|
+
}
|
|
478
|
+
onStack.delete(name);
|
|
479
|
+
stack.pop();
|
|
480
|
+
done.add(name);
|
|
481
|
+
};
|
|
482
|
+
for (const name of remaining) {
|
|
483
|
+
if (!done.has(name)) walk(name);
|
|
484
|
+
if (cycle) break;
|
|
485
|
+
}
|
|
486
|
+
return `[arc] module dependency cycle: ${cycle ? cycle.join(" → ") : [...remaining].join(", ")}. Modules cannot dependsOn each other circularly — break it with a shared module both point at, or wire the softer direction through an event/port instead of a hard dependsOn.`;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Identity helper for authoring a typed module — mirrors `defineResource`.
|
|
490
|
+
* Gives inference + a single obvious construction site; does no work.
|
|
491
|
+
*
|
|
492
|
+
* `TExports` is inferred from the `bootstrap` return value, so a module
|
|
493
|
+
* author gets a typed public export for free:
|
|
494
|
+
*
|
|
495
|
+
* ```ts
|
|
496
|
+
* export const accountingModule = (deps: Deps) =>
|
|
497
|
+
* defineModule({
|
|
498
|
+
* name: "accounting",
|
|
499
|
+
* bootstrap: async () => createAccountingEngine(deps), // TExports inferred
|
|
500
|
+
* });
|
|
501
|
+
* ```
|
|
502
|
+
*/
|
|
503
|
+
function defineModule(module) {
|
|
504
|
+
return module;
|
|
505
|
+
}
|
|
506
|
+
function getModuleExports(fastify, name) {
|
|
507
|
+
const modules = fastify.arc?.modules;
|
|
508
|
+
if (!modules || !Object.hasOwn(modules, name)) {
|
|
509
|
+
const available = modules ? Object.keys(modules) : [];
|
|
510
|
+
throw new Error(`[arc] no public export recorded for module "${name}".\nA module's export is its bootstrap() return value; it is readable only AFTER that module's bootstrap ran (dependsOn / list order = init order).\nModules with recorded exports: ${available.length ? available.join(", ") : "(none)"}.`);
|
|
511
|
+
}
|
|
512
|
+
return modules[name];
|
|
513
|
+
}
|
|
514
|
+
//#endregion
|
|
387
515
|
//#region src/factory/registerResources.ts
|
|
388
516
|
function isResourcesFactory(value) {
|
|
389
517
|
return typeof value === "function";
|
|
@@ -401,15 +529,29 @@ async function registerOne(parent, resource) {
|
|
|
401
529
|
}
|
|
402
530
|
}
|
|
403
531
|
/**
|
|
404
|
-
* Execute the full resource lifecycle
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
532
|
+
* Execute the full resource lifecycle. The module graph is resolved + ordered
|
|
533
|
+
* by `dependsOn` (stable topological sort) once, up FRONT — before any
|
|
534
|
+
* side-effecting plugin — so a bad graph fails fast with no half-initialised
|
|
535
|
+
* infra. Every phase below iterates that order; each module phase runs BEFORE
|
|
536
|
+
* its app-level counterpart:
|
|
537
|
+
*
|
|
538
|
+
* 1. resolve + orderModules — validate the graph (dup names, missing/self
|
|
539
|
+
* deps, cycles fail fast) BEFORE side effects
|
|
540
|
+
* 2. plugins() — app infra (DB, data, webhooks)
|
|
541
|
+
* 3. module.plugins — module infra, dependsOn order, before bootstraps
|
|
542
|
+
* 4a. module.bootstrap — engine init (return value → arc.modules[name])
|
|
543
|
+
* 4b. bootstrap[] — app-level domain init
|
|
544
|
+
* 5. resources factory (if any) — resolved AFTER bootstrap, so engine-backed
|
|
408
545
|
* adapters can `await ensureEngine()` and pass
|
|
409
546
|
* live models/repos into `defineResource(...)`
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
547
|
+
* 5b. module resources — RESOLVED after the app factory, but PREPENDED
|
|
548
|
+
* so they REGISTER before app resources (two
|
|
549
|
+
* orderings, both intentional — see §5b)
|
|
550
|
+
* 6. module.afterResources → afterResources() — post-registration wiring
|
|
551
|
+
* 7. onReady — lifecycle hook
|
|
552
|
+
* 7b. onClose — module.onClose (reverse composition order)
|
|
553
|
+
* THEN app onClose, in ONE hook so module
|
|
554
|
+
* teardown runs before shared infra closes.
|
|
413
555
|
*/
|
|
414
556
|
async function registerResources(fastify, config) {
|
|
415
557
|
if (config.preset === "production") {
|
|
@@ -422,10 +564,37 @@ async function registerResources(fastify, config) {
|
|
|
422
564
|
strictResourceDir: true
|
|
423
565
|
};
|
|
424
566
|
}
|
|
567
|
+
const modules = orderModules(await Promise.all((config.modules ?? []).map((m) => resolveModule(m))));
|
|
425
568
|
if (config.plugins) {
|
|
426
569
|
await config.plugins(fastify);
|
|
427
570
|
fastify.log.debug("Custom plugins registered");
|
|
428
571
|
}
|
|
572
|
+
for (const m of modules) {
|
|
573
|
+
if (!m.plugins) continue;
|
|
574
|
+
try {
|
|
575
|
+
await m.plugins(fastify);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
578
|
+
throw new Error(`[arc] module "${m.name}" plugins() threw: ${msg}`, { cause: err });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const m of modules) {
|
|
582
|
+
if (!m.bootstrap) continue;
|
|
583
|
+
let exported;
|
|
584
|
+
try {
|
|
585
|
+
exported = await m.bootstrap(fastify);
|
|
586
|
+
} catch (err) {
|
|
587
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
588
|
+
throw new Error(`[arc] module "${m.name}" bootstrap threw: ${msg}`, { cause: err });
|
|
589
|
+
}
|
|
590
|
+
if (exported !== void 0) {
|
|
591
|
+
const arc = fastify.arc;
|
|
592
|
+
if (!arc) throw new Error(`[arc] module "${m.name}" returned a public export but fastify.arc is not decorated — arcCorePlugin must register first.`);
|
|
593
|
+
if (!arc.modules) arc.modules = Object.create(null);
|
|
594
|
+
arc.modules[m.name] = exported;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (modules.length) fastify.log.debug(`${modules.length} module(s) composed (in dependency order): ${modules.map((m) => m.name).join(" → ")}`);
|
|
429
598
|
if (config.bootstrap?.length) {
|
|
430
599
|
for (const init of config.bootstrap) await init(fastify);
|
|
431
600
|
fastify.log.debug(`${config.bootstrap.length} bootstrap function(s) executed`);
|
|
@@ -462,6 +631,20 @@ async function registerResources(fastify, config) {
|
|
|
462
631
|
}
|
|
463
632
|
resolvedResources = discovered;
|
|
464
633
|
}
|
|
634
|
+
if (modules.length) {
|
|
635
|
+
const moduleResources = [];
|
|
636
|
+
for (const m of modules) {
|
|
637
|
+
if (!m.resources) continue;
|
|
638
|
+
try {
|
|
639
|
+
const rs = typeof m.resources === "function" ? await m.resources(fastify) : m.resources;
|
|
640
|
+
moduleResources.push(...rs);
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
643
|
+
throw new Error(`[arc] module "${m.name}" resources factory threw: ${msg}. Check the module's bootstrap ran and its adapters received booted engines.`, { cause: err });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (moduleResources.length) resolvedResources = [...moduleResources, ...resolvedResources ?? []];
|
|
647
|
+
}
|
|
465
648
|
if (resolvedResources && resolvedResources.length > 0) {
|
|
466
649
|
const seen = /* @__PURE__ */ new Set();
|
|
467
650
|
for (const resource of resolvedResources) if (resource.name) {
|
|
@@ -490,6 +673,15 @@ async function registerResources(fastify, config) {
|
|
|
490
673
|
const hints = discoveryYieldedZero ? ` but yielded 0 resources. Check the path, file naming (*.resource.{ts,js,mts,mjs}), and runtime layout (src/ vs dist/). Use \`strictResourceDir: true\` to fail boot.` : "";
|
|
491
674
|
fastify.log.warn(`0 resources registered${prefix}${scanned}${hints}`);
|
|
492
675
|
}
|
|
676
|
+
for (const m of modules) {
|
|
677
|
+
if (!m.afterResources) continue;
|
|
678
|
+
try {
|
|
679
|
+
await m.afterResources(fastify);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
682
|
+
throw new Error(`[arc] module "${m.name}" afterResources() threw: ${msg}`, { cause: err });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
493
685
|
if (config.afterResources) {
|
|
494
686
|
await config.afterResources(fastify);
|
|
495
687
|
fastify.log.debug("afterResources hook executed");
|
|
@@ -500,12 +692,12 @@ async function registerResources(fastify, config) {
|
|
|
500
692
|
await onReady(fastify);
|
|
501
693
|
});
|
|
502
694
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
695
|
+
const closers = modules.filter((m) => m.onClose).reverse();
|
|
696
|
+
const appOnClose = config.onClose;
|
|
697
|
+
if (closers.length || appOnClose) fastify.addHook("onClose", async () => {
|
|
698
|
+
for (const m of closers) await m.onClose?.(fastify);
|
|
699
|
+
if (appOnClose) await appOnClose(fastify);
|
|
700
|
+
});
|
|
509
701
|
}
|
|
510
702
|
//#endregion
|
|
511
703
|
//#region src/factory/registerSecurity.ts
|
|
@@ -522,7 +714,8 @@ function buildRateLimitOpts(input) {
|
|
|
522
714
|
};
|
|
523
715
|
const matchesPath = compilePathMatcher(skipPaths);
|
|
524
716
|
const combined = async (req, key) => {
|
|
525
|
-
|
|
717
|
+
const path = (req.url ?? "").split("?", 1)[0] ?? "";
|
|
718
|
+
if (matchesPath(path)) return true;
|
|
526
719
|
if (typeof allowList === "function") return await allowList(req, key);
|
|
527
720
|
if (Array.isArray(allowList)) return allowList.includes(key);
|
|
528
721
|
return false;
|
|
@@ -711,7 +904,7 @@ var createApp_exports = /* @__PURE__ */ __exportAll({
|
|
|
711
904
|
createApp: () => createApp,
|
|
712
905
|
resolveLoggerConfig: () => resolveLoggerConfig
|
|
713
906
|
});
|
|
714
|
-
const MEMORY_STORE_NAMES = new Set(["memory", "memory-cache"]);
|
|
907
|
+
const MEMORY_STORE_NAMES = /* @__PURE__ */ new Set(["memory", "memory-cache"]);
|
|
715
908
|
/**
|
|
716
909
|
* Default redact paths layered into Fastify's pino logger when the host
|
|
717
910
|
* doesn't supply a `logger.redact` of their own. Covers the common token /
|
|
@@ -947,4 +1140,4 @@ const ArcFactory = {
|
|
|
947
1140
|
}
|
|
948
1141
|
};
|
|
949
1142
|
//#endregion
|
|
950
|
-
export {
|
|
1143
|
+
export { getModuleExports as a, developmentPreset as c, productionPreset as d, testingPreset as f, defineModule as i, edgePreset as l, createApp as n, orderModules as o, createApp_exports as r, resolveModule as s, ArcFactory as t, getPreset as u };
|
package/dist/docs/index.d.mts
CHANGED
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as getUserRoles } from "../types-XTApa8b6.mjs";
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-Dq6RbEGK.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
2
|
+
|
|
3
|
+
//#region src/encryption/types.d.ts
|
|
4
|
+
/** JWE key-management algorithm — the protected-header `alg`. */
|
|
5
|
+
type JweAlg = "RSA-OAEP-256" | "RSA-OAEP-384" | "RSA-OAEP-512" | "ECDH-ES" | "ECDH-ES+A256KW" | "A256GCMKW" | "dir";
|
|
6
|
+
/** JWE content-encryption algorithm — the protected-header `enc`. AEAD only. */
|
|
7
|
+
type JweEnc = "A128GCM" | "A192GCM" | "A256GCM";
|
|
8
|
+
/** Encryption strategy for a route's response. */
|
|
9
|
+
type EncryptionMode = "jwe" | "fields";
|
|
10
|
+
/**
|
|
11
|
+
* Opaque key material. For `jwe` mode this is a `jose` `KeyLike` /
|
|
12
|
+
* `CryptoKey` / `Uint8Array`; for `fields` mode a 32-byte secret
|
|
13
|
+
* (`Buffer` / `Uint8Array`). Kept `unknown` at the contract layer so the
|
|
14
|
+
* `KeyProvider` interface carries no hard `jose` type dependency — hosts
|
|
15
|
+
* without `jose` installed can still implement field-mode providers.
|
|
16
|
+
*/
|
|
17
|
+
type KeyMaterial = unknown;
|
|
18
|
+
/** A key paired with its identifier — what encrypt operations consume. */
|
|
19
|
+
interface ActiveKey {
|
|
20
|
+
/** Key id, stamped into the JWE `kid` header / field envelope for rotation. */
|
|
21
|
+
readonly kid: string;
|
|
22
|
+
/** The key material (public key for JWE, 32-byte secret for fields). */
|
|
23
|
+
readonly key: KeyMaterial;
|
|
24
|
+
/** Optional per-key `alg` override (else the plugin default applies). */
|
|
25
|
+
readonly alg?: JweAlg;
|
|
26
|
+
}
|
|
27
|
+
/** Context handed to the key provider so resolution can be request-scoped. */
|
|
28
|
+
interface KeyContext {
|
|
29
|
+
readonly request: FastifyRequest;
|
|
30
|
+
}
|
|
31
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
32
|
+
/**
|
|
33
|
+
* Pluggable key resolution. Arc owns the request-time plumbing — `kid` →
|
|
34
|
+
* key, algorithm allowlist enforcement; the host owns the trust store —
|
|
35
|
+
* which keys are valid and where they live (file / env / AWS KMS / GCP KMS
|
|
36
|
+
* / Vault). Encryption and decryption are deliberately separate operations
|
|
37
|
+
* because they often use DIFFERENT keys (JWE: a public key encrypts the
|
|
38
|
+
* outbound response, a private key by `kid` decrypts the inbound request —
|
|
39
|
+
* exactly Visa's two-key-pair model).
|
|
40
|
+
*
|
|
41
|
+
* Rotation mirrors `@fastify/secure-session`: the active key encrypts; any
|
|
42
|
+
* key resolvable by `kid` can decrypt, so you can roll keys without dropping
|
|
43
|
+
* in-flight traffic.
|
|
44
|
+
*/
|
|
45
|
+
interface KeyProvider {
|
|
46
|
+
/** Active key for encrypting OUTBOUND responses. */
|
|
47
|
+
encryptionKey(ctx: KeyContext): MaybePromise<ActiveKey>;
|
|
48
|
+
/** Resolve a DECRYPTION key by the inbound message's `kid`. */
|
|
49
|
+
decryptionKey(kid: string, ctx: KeyContext): MaybePromise<KeyMaterial | undefined>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Per-resource encryption directive — the typed slice plugins register on
|
|
53
|
+
* `ResourceExtensions`. Declared on a resource via
|
|
54
|
+
* `defineResource({ extensions: { encryption: { … } } })`; arc threads it
|
|
55
|
+
* to request time on `request.routeOptions.config.arcExtensions.encryption`.
|
|
56
|
+
*/
|
|
57
|
+
interface EncryptionDirective {
|
|
58
|
+
/** Strategy. Defaults to the plugin-level `mode` (default `'jwe'`). */
|
|
59
|
+
readonly mode?: EncryptionMode;
|
|
60
|
+
/**
|
|
61
|
+
* For `mode: 'fields'` — dot-paths of fields to encrypt, resolved against
|
|
62
|
+
* the serialized response body (e.g. `'cardNumber'`, `'account.iban'`).
|
|
63
|
+
* Ignored for `mode: 'jwe'` (the whole body is encrypted).
|
|
64
|
+
*/
|
|
65
|
+
readonly fields?: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* Explicit opt-out — set `false` to disable encryption for this resource
|
|
68
|
+
* even when a plugin-level default (`routes`) would otherwise apply.
|
|
69
|
+
*/
|
|
70
|
+
readonly enabled?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Plugin-level options. `keyProvider` is the only required field; everything
|
|
74
|
+
* else has a secure default.
|
|
75
|
+
*/
|
|
76
|
+
interface EncryptionOptions {
|
|
77
|
+
/**
|
|
78
|
+
* Master on/off. Default `true`. Set `false` to register the plugin as a
|
|
79
|
+
* typed no-op (config-gated environments) — same pattern as
|
|
80
|
+
* `idempotencyPlugin`.
|
|
81
|
+
*/
|
|
82
|
+
readonly enabled?: boolean;
|
|
83
|
+
/** Key provider — resolves `kid` → key for encrypt/decrypt. Required. */
|
|
84
|
+
readonly keyProvider: KeyProvider;
|
|
85
|
+
/** Default mode when a route opts in without naming one. Default `'jwe'`. */
|
|
86
|
+
readonly mode?: EncryptionMode;
|
|
87
|
+
/** JWE key-management algorithm. Default `'RSA-OAEP-256'`. */
|
|
88
|
+
readonly alg?: JweAlg;
|
|
89
|
+
/** JWE content-encryption algorithm. Default `'A256GCM'`. */
|
|
90
|
+
readonly enc?: JweEnc;
|
|
91
|
+
/**
|
|
92
|
+
* Inbound-decryption algorithm allowlists — block algorithm-substitution
|
|
93
|
+
* downgrade attacks. Default `[alg]` / `[enc]`. Set explicitly only when a
|
|
94
|
+
* client legitimately uses more than one algorithm.
|
|
95
|
+
*/
|
|
96
|
+
readonly allowedAlgs?: readonly JweAlg[];
|
|
97
|
+
readonly allowedEncs?: readonly JweEnc[];
|
|
98
|
+
/**
|
|
99
|
+
* Fallback opt-in for routes that don't declare
|
|
100
|
+
* `extensions.encryption`. Return a directive (or `true` for the default
|
|
101
|
+
* mode) to encrypt, `false`/`undefined` to skip. The per-resource
|
|
102
|
+
* `extensions.encryption` ALWAYS wins when present. Use this to scope ALE
|
|
103
|
+
* to sensitive prefixes without touching every resource.
|
|
104
|
+
*
|
|
105
|
+
* @example confine ALE to `/payments` and `/accounts`
|
|
106
|
+
* ```ts
|
|
107
|
+
* routes: (req) => /^\/(payments|accounts)\b/.test(req.url)
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
readonly routes?: (request: FastifyRequest) => EncryptionDirective | boolean | undefined;
|
|
111
|
+
/** Decrypt inbound `application/jose` request bodies. Default `true`. */
|
|
112
|
+
readonly decryptRequests?: boolean;
|
|
113
|
+
/** Media type that signals an encrypted REQUEST body. Default `'application/jose'`. */
|
|
114
|
+
readonly contentType?: string;
|
|
115
|
+
/** Content-Type set on full-body JWE RESPONSES. Default `'application/jose'`. */
|
|
116
|
+
readonly responseContentType?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Response header advertising that the body is encrypted, so generic
|
|
119
|
+
* client SDKs know to decrypt. Default `'x-encrypted'`; set `false` to omit.
|
|
120
|
+
*/
|
|
121
|
+
readonly flagHeader?: string | false;
|
|
122
|
+
}
|
|
123
|
+
/** Request decorations added by the plugin. */
|
|
124
|
+
declare module "fastify" {
|
|
125
|
+
interface FastifyRequest {
|
|
126
|
+
/** `true` when the inbound body was decrypted from a JWE by this plugin. */
|
|
127
|
+
requestDecrypted?: boolean;
|
|
128
|
+
/** `true` when the outbound body was encrypted by this plugin. */
|
|
129
|
+
responseEncrypted?: boolean;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/encryption/encryptionPlugin.d.ts
|
|
134
|
+
declare const encryptionPlugin: FastifyPluginAsync<EncryptionOptions>;
|
|
135
|
+
declare const _default: FastifyPluginAsync<EncryptionOptions>;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/encryption/fieldCipher.d.ts
|
|
138
|
+
/**
|
|
139
|
+
* Field-level cipher — `node:crypto` AES-256-GCM, zero external deps.
|
|
140
|
+
*
|
|
141
|
+
* Used by `mode: 'fields'` to encrypt individual field values in place while
|
|
142
|
+
* leaving the JSON structure intact. The envelope is an authenticated,
|
|
143
|
+
* versioned, `kid`-tagged token:
|
|
144
|
+
*
|
|
145
|
+
* arc.v1.<b64url(kid)>.<b64url(iv)>.<b64url(ciphertext)>.<b64url(tag)>
|
|
146
|
+
*
|
|
147
|
+
* This is NOT a hand-rolled cipher — it's the correct use of a vetted AEAD
|
|
148
|
+
* primitive (AES-256-GCM): a fresh random 96-bit IV per operation (never
|
|
149
|
+
* caller-supplied — IV reuse under GCM is catastrophic), authentication via
|
|
150
|
+
* the GCM tag, and a version prefix so the format can evolve. Same shape, in
|
|
151
|
+
* spirit, as MongoDB CSFLE's BSON-binary subtype-6 envelope. For
|
|
152
|
+
* cross-vendor interop or asymmetric keys use `mode: 'jwe'` (RFC 7516)
|
|
153
|
+
* instead.
|
|
154
|
+
*/
|
|
155
|
+
/** Encrypt a UTF-8 string into the `arc.v1` field envelope. */
|
|
156
|
+
declare function encryptField(plaintext: string, kid: string, key: unknown): string;
|
|
157
|
+
/** Parsed envelope header — exposes `kid` for key resolution before decrypt. */
|
|
158
|
+
interface ParsedFieldEnvelope {
|
|
159
|
+
readonly kid: string;
|
|
160
|
+
readonly iv: Buffer;
|
|
161
|
+
readonly ciphertext: Buffer;
|
|
162
|
+
readonly tag: Buffer;
|
|
163
|
+
}
|
|
164
|
+
/** Returns the parsed envelope, or `null` when the token isn't `arc.v1`. */
|
|
165
|
+
declare function parseFieldEnvelope(token: string): ParsedFieldEnvelope | null;
|
|
166
|
+
/** Decrypt a parsed `arc.v1` envelope with the resolved key. Throws on tamper. */
|
|
167
|
+
declare function decryptField(envelope: ParsedFieldEnvelope, key: unknown): string;
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/encryption/keyProvider.d.ts
|
|
170
|
+
interface StaticKeyProviderOptions {
|
|
171
|
+
/**
|
|
172
|
+
* Active key used to ENCRYPT outbound responses. For `jwe` mode this is the
|
|
173
|
+
* recipient's PUBLIC key; for `fields` mode a 32-byte symmetric secret.
|
|
174
|
+
*/
|
|
175
|
+
readonly encryptionKey: ActiveKey;
|
|
176
|
+
/**
|
|
177
|
+
* Keys used to DECRYPT inbound requests, indexed by `kid`. For `jwe` mode
|
|
178
|
+
* these are PRIVATE keys; for `fields` mode the same symmetric secrets.
|
|
179
|
+
*
|
|
180
|
+
* Defaults to `{ [encryptionKey.kid]: encryptionKey.key }` — correct for
|
|
181
|
+
* symmetric field mode where one key both encrypts and decrypts. For
|
|
182
|
+
* asymmetric JWE you MUST pass the private key(s) here; the public
|
|
183
|
+
* `encryptionKey` cannot decrypt. Keep 2–3 entries during a rotation so
|
|
184
|
+
* in-flight messages signed with the previous `kid` still decrypt.
|
|
185
|
+
*/
|
|
186
|
+
readonly decryptionKeys?: Readonly<Record<string, KeyMaterial>>;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A static, in-memory key provider with `kid`-based rotation. Keys are held
|
|
190
|
+
* for the process lifetime — resolve them from env / a secrets file at boot
|
|
191
|
+
* and pass the imported key material in (don't re-parse PEM per request).
|
|
192
|
+
*
|
|
193
|
+
* @example symmetric field mode
|
|
194
|
+
* ```ts
|
|
195
|
+
* const provider = createStaticKeyProvider({
|
|
196
|
+
* encryptionKey: { kid: "2026-06", key: Buffer.from(process.env.FIELD_KEY!, "base64") },
|
|
197
|
+
* });
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @example asymmetric JWE (jose key material)
|
|
201
|
+
* ```ts
|
|
202
|
+
* const provider = createStaticKeyProvider({
|
|
203
|
+
* encryptionKey: { kid: "client-1", key: await importSPKI(clientPubPem, "RSA-OAEP-256") },
|
|
204
|
+
* decryptionKeys: { "server-1": await importPKCS8(serverPrivPem, "RSA-OAEP-256") },
|
|
205
|
+
* });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
declare function createStaticKeyProvider(options: StaticKeyProviderOptions): KeyProvider;
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/encryption/index.d.ts
|
|
211
|
+
declare module "../types/resource.js" {
|
|
212
|
+
interface ResourceExtensions {
|
|
213
|
+
/**
|
|
214
|
+
* Encrypt this resource's responses — full-body JWE or specific fields.
|
|
215
|
+
* Threaded to request time on
|
|
216
|
+
* `request.routeOptions.config.arcExtensions.encryption`.
|
|
217
|
+
*/
|
|
218
|
+
encryption?: EncryptionDirective;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
//#endregion
|
|
222
|
+
export { type ActiveKey, type EncryptionDirective, type EncryptionMode, type EncryptionOptions, type JweAlg, type JweEnc, type KeyContext, type KeyMaterial, type KeyProvider, type ParsedFieldEnvelope, type StaticKeyProviderOptions, createStaticKeyProvider, decryptField, _default as default, encryptField, encryptionPlugin, parseFieldEnvelope };
|