@classytic/arc 2.18.4 → 2.19.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/dist/{BaseController-B-uXzf6I.mjs → BaseController-Dy6EQHjM.mjs} +7 -7
- package/dist/{QueryCache-Dsyv2mLS.d.mts → QueryCache-TRfXau3W.d.mts} +1 -1
- package/dist/{ResourceRegistry-CWmOggxS.mjs → ResourceRegistry-5fKZwhK_.mjs} +1 -1
- package/dist/{pipeline-D4sqiokR.mjs → actionPermissions-D7NGwcAR.mjs} +29 -5
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +4 -4
- package/dist/auth/index.mjs +262 -6
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +2 -0
- package/dist/{buildHandler-Cvrc2p7r.mjs → buildHandler-B0qvAguf.mjs} +1 -1
- package/dist/cache/index.d.mts +3 -3
- package/dist/cache/index.mjs +3 -3
- package/dist/{caching-C0DdKiYB.mjs → caching-_NJeDd1_.mjs} +1 -1
- 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.d.mts +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.d.mts +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-C88uIwd3.mjs → core-Ba-6Sue6.mjs} +67 -63
- package/dist/{createActionRouter-DGCCp-kP.mjs → createActionRouter-B_WzBFyf.mjs} +10 -10
- package/dist/{createAggregationRouter-Ca77b8iK.mjs → createAggregationRouter-B_G_nIjm.mjs} +4 -4
- package/dist/{createApp-J1iPXY9p.mjs → createApp--703hQE1.mjs} +14 -14
- package/dist/docs/index.d.mts +2 -2
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-BzhyVWfn.mjs → elevation-COpYKt2h.mjs} +2 -2
- package/dist/encryption/index.d.mts +222 -0
- package/dist/encryption/index.mjs +270 -0
- package/dist/{errorHandler-Bua97SEt.mjs → errorHandler-Dxf6P_kV.mjs} +2 -2
- package/dist/{eventPlugin-Bw4bUDEO.mjs → eventPlugin-DJG2D7sy.mjs} +106 -5
- package/dist/{eventPlugin-C1W1CRbf.d.mts → eventPlugin-DbBFd3x_.d.mts} +1 -1
- package/dist/events/index.d.mts +3 -3
- package/dist/events/index.mjs +189 -5
- 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 +3 -3
- package/dist/{fastify-augmentation-BdCl_eNo.d.mts → fastify-augmentation-D-5_yQJi.d.mts} +2 -2
- package/dist/{fields-CKLrhyOb.d.mts → fields-C7ahdGYf.d.mts} +1 -1
- package/dist/{generate-BaxDdmJ8.mjs → generate-Brans3fW.mjs} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/hooks/index.mjs +1 -1
- package/dist/idempotency/index.d.mts +2 -2
- package/dist/idempotency/index.mjs +1 -1
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/{index-Cvo1u9n6.d.mts → index-CP6A1Ffj.d.mts} +9 -9
- package/dist/{index-vcWUGkwf.d.mts → index-CRKiiP5o.d.mts} +3 -3
- package/dist/{index-QYma8mdC.d.mts → index-CUTG2Cuo.d.mts} +88 -7
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +7 -7
- package/dist/{init-DSsdRfk9.mjs → init-Bqpt1Hxf.mjs} +274 -130
- package/dist/integrations/event-gateway.d.mts +2 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/mcp/index.d.mts +4 -4
- 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.mjs +7 -3
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/{loadResources-CwXt9MCh.mjs → loadResources-B8UmpITt.mjs} +1 -1
- package/dist/{loadResourcesFromEntry-XHNeUWpu.mjs → loadResourcesFromEntry-BTlFEx05.mjs} +1 -1
- package/dist/{mcpPlugin-CzKdt3B8.mjs → mcpPlugin-C088Nd-G.mjs} +10 -11
- package/dist/{memory-TdTHCS9-.mjs → memory-CLXA2VeJ.mjs} +1 -1
- package/dist/{metrics-Da2EUPi9.mjs → metrics-fdxI5mn_.mjs} +1 -1
- package/dist/middleware/index.d.mts +1 -1
- package/dist/middleware/index.mjs +2 -2
- package/dist/{openapi-ButnRyOM.mjs → openapi-DZ-5a5Op.mjs} +5 -5
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-3uzwkbQt.mjs → permissions-BPB1xqGf.mjs} +4 -4
- package/dist/{pipe-9Tl-OAus.mjs → pipe-BOLmGIxa.mjs} +1 -1
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/pipeline/index.mjs +1 -1
- package/dist/plugins/index.d.mts +6 -7
- package/dist/plugins/index.mjs +12 -12
- 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/presets/filesUpload.d.mts +4 -4
- package/dist/presets/filesUpload.mjs +4 -4
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +2 -2
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-CF79RHEo.mjs → presets-BtyFwTO4.mjs} +2 -2
- package/dist/{queryCachePlugin-CnAeFqju.mjs → queryCachePlugin-emHb0CuX.mjs} +4 -4
- package/dist/{queryCachePlugin-Bd2tYgh5.d.mts → queryCachePlugin-xzFyKAjT.d.mts} +2 -2
- package/dist/{redis-3hqH_Qk_.d.mts → redis-C3W0qbz8.d.mts} +1 -1
- package/dist/{redis-stream-BtEUtjxZ.d.mts → redis-stream-DsP1vqZB.d.mts} +1 -1
- package/dist/{rateLimit-yKvxWAky.mjs → registerRoute-BdV8DDwU.mjs} +127 -2
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +2 -2
- package/dist/{registry-Br15xdGM.mjs → registry-Bm9LjWKs.mjs} +1 -1
- package/dist/{replyHelpers-DXHVxrrj.mjs → replyHelpers-67tRawQZ.mjs} +1 -1
- package/dist/{schemaIR-CE6USIRi.mjs → schemaIR-D1MJwqmp.mjs} +1 -1
- package/dist/schemas/index.d.mts +64 -65
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +3 -3
- package/dist/{sse-BrY1W6U8.mjs → sse-CfUjwjDk.mjs} +7 -4
- package/dist/streaming-BQcW6_Kq.mjs +162 -0
- package/dist/testing/index.d.mts +7 -7
- package/dist/testing/index.mjs +3 -3
- package/dist/testing/storageContract.d.mts +1 -1
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +1 -1
- package/dist/{types-WVlAVp8T.d.mts → types-9_wtNPsI.d.mts} +10 -10
- package/dist/{types-BWBcb88c.d.mts → types-DpvSF5Hh.d.mts} +1 -1
- package/dist/{types-Bi0r0vjG.mjs → types-bHblZ9YN.mjs} +1 -1
- package/dist/utils/index.d.mts +42 -4
- package/dist/utils/index.mjs +6 -5
- package/dist/{utils-xAQeDyU_.mjs → utils-C6QQ9-hd.mjs} +1415 -1524
- package/dist/{validate-0__MuUsK.mjs → validate-CRcaWzp2.mjs} +1 -1
- package/dist/{versioning-BSEDGXKC.d.mts → versioning-D_eBCfNr.d.mts} +8 -1
- package/dist/{versioning-C0K8KQLx.mjs → versioning-Dk-71iIz.mjs} +1 -1
- package/dist/{websocket-BFBT5rH-.d.mts → websocket-MlYlrYq0.d.mts} +1 -1
- package/package.json +45 -30
- package/dist/EventTransport-7LPXLEUX.mjs +0 -103
- package/dist/actionPermissions-CX9QIMTd.mjs +0 -24
- package/dist/betterAuthOpenApi-DAAhMZff.mjs +0 -251
- package/dist/defineEvent-DC981g-o.mjs +0 -188
- package/dist/registerRoute-BEYn0ylk.mjs +0 -108
- /package/dist/{EventTransport-CaaXM5Y1.d.mts → EventTransport-IEn7Tn-Q.d.mts} +0 -0
- /package/dist/{HookSystem-BfCpWbwM.mjs → HookSystem-C1vB_Gc9.mjs} +0 -0
- /package/dist/{constants-TrJVIJl0.mjs → constants-gaEeMlz-.mjs} +0 -0
- /package/dist/{elevation-DW90z5bT.d.mts → elevation-xGUBq6p_.d.mts} +0 -0
- /package/dist/{errorHandler-_Lp2k9M3.d.mts → errorHandler-BtUUDPEa.d.mts} +0 -0
- /package/dist/{errors-DZOemeGa.d.mts → errors-DOHkjQlD.d.mts} +0 -0
- /package/dist/{errors-j4aJm1Wg.mjs → errors-DkO8Nk6-.mjs} +0 -0
- /package/dist/{externalPaths-DLle0R07.d.mts → externalPaths-BE6wFpYv.d.mts} +0 -0
- /package/dist/{generate-Dmk8cfvX.d.mts → generate-BnsL9GBm.d.mts} +0 -0
- /package/dist/{init-BS4Daqnv.d.mts → init-BPEu8cs-.d.mts} +0 -0
- /package/dist/{interface-DL7LlWS5.d.mts → interface-C1_WLTMK.d.mts} +0 -0
- /package/dist/{interface-CsrGR8LF.d.mts → interface-Uk8ONJv7.d.mts} +0 -0
- /package/dist/{keys-B3yyBEyH.mjs → keys-Cd316u_y.mjs} +0 -0
- /package/dist/{multipartBody-709YcXi1.mjs → multipartBody-CHEQSZ6G.mjs} +0 -0
- /package/dist/{pluralize-BxdUl2P4.mjs → pluralize-DccFV9R7.mjs} +0 -0
- /package/dist/{preloadResources-B-j-iqo-.mjs → preloadResources-CHw3_oA_.mjs} +0 -0
- /package/dist/{pushref-registry-Dqclzjy7.d.mts → pushref-registry-DnCqL3aa.d.mts} +0 -0
- /package/dist/{requestContext-DDLS3NVU.mjs → requestContext-BZKOr13H.mjs} +0 -0
- /package/dist/{chunk-BpYLSNr0.mjs → rolldown-runtime-8H4AJuhK.mjs} +0 -0
- /package/dist/{schemaConverter-DatAaVNj.mjs → schemaConverter-U0MnO9vd.mjs} +0 -0
- /package/dist/{sessionManager-CyHpYXoB.d.mts → sessionManager-BXG-HGwG.d.mts} +0 -0
- /package/dist/{storage-C5q8fuEt.d.mts → storage-Co_PQpeQ.d.mts} +0 -0
- /package/dist/{store-helpers-DjSYI2uP.mjs → store-helpers-DY6xq4sM.mjs} +0 -0
- /package/dist/{tracing-Bnb1-YVd.d.mts → tracing-D8jlGXP7.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-C9KXUly6.mjs} +0 -0
- /package/dist/{types-BJwJN2Pg.d.mts → types-LNebCoJx.d.mts} +0 -0
- /package/dist/{types-D57iXYb8.mjs → types-XTApa8b6.mjs} +0 -0
|
@@ -1,58 +1,18 @@
|
|
|
1
|
-
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-
|
|
1
|
+
import { s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS } from "./constants-gaEeMlz-.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import { r as ForbiddenError } from "./errors-
|
|
4
|
-
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-
|
|
5
|
-
import {
|
|
6
|
-
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { t as
|
|
10
|
-
import {
|
|
11
|
-
import { t as
|
|
12
|
-
import { n as
|
|
13
|
-
import { t as
|
|
14
|
-
import {
|
|
15
|
-
import { t as pluralize } from "./pluralize-
|
|
16
|
-
import { t as resolveActionPermission } from "./actionPermissions-CX9QIMTd.mjs";
|
|
17
|
-
//#region src/core/middlewares/fieldWrite.ts
|
|
18
|
-
/**
|
|
19
|
-
* Build a preHandler that enforces field-write permissions on the request body.
|
|
20
|
-
*
|
|
21
|
-
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
22
|
-
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
23
|
-
* never went through that path — so a host that declared
|
|
24
|
-
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
25
|
-
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
26
|
-
*
|
|
27
|
-
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
28
|
-
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
29
|
-
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
30
|
-
* generate surprising 403s by enforcing rules tied to that shape.
|
|
31
|
-
*
|
|
32
|
-
* Returns `null` when the resource has no field permissions configured — the
|
|
33
|
-
* caller should then skip wiring it into the preHandler chain.
|
|
34
|
-
*/
|
|
35
|
-
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
36
|
-
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
37
|
-
return async (request, _reply) => {
|
|
38
|
-
const body = request.body;
|
|
39
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
40
|
-
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
41
|
-
if (isElevated(scope)) return;
|
|
42
|
-
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
43
|
-
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
44
|
-
request.body = filtered;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* True for HTTP methods that carry a request body and should run the
|
|
49
|
-
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
50
|
-
*/
|
|
51
|
-
function methodCarriesBody(method) {
|
|
52
|
-
const m = method.toUpperCase();
|
|
53
|
-
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
3
|
+
import { r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
+
import { b as isElevated, n as PUBLIC_SCOPE, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
5
|
+
import { p as assertValidConfig, x as getDefaultCrudSchemas } from "./utils-C6QQ9-hd.mjs";
|
|
6
|
+
import { d as DEFAULT_FIELD_WRITE_DENIAL_POLICY, t as BaseController } from "./BaseController-Dy6EQHjM.mjs";
|
|
7
|
+
import { F as applyFieldWritePermissions, L as resolveEffectiveRoles } from "./permissions-BPB1xqGf.mjs";
|
|
8
|
+
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
9
|
+
import { n as convertRouteSchema, t as convertOpenApiSchemas } from "./schemaConverter-U0MnO9vd.mjs";
|
|
10
|
+
import { i as pipeUIMessageStreamToReply, r as isReadableStream } from "./streaming-BQcW6_Kq.mjs";
|
|
11
|
+
import { a as createCrudHandlers, d as buildRequestScopeProjection, i as resolvePipelineSteps, o as createFastifyHandler, r as buildPipelineHandler, s as createRequestContext, t as resolveActionPermission } from "./actionPermissions-D7NGwcAR.mjs";
|
|
12
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, l as resolveRoutePreHandlers, n as buildRateLimitConfig, r as buildRouteConfig, s as buildCrudPermissionMw, t as tryRegisterRoute, u as buildAuthMiddleware } from "./registerRoute-BdV8DDwU.mjs";
|
|
13
|
+
import { t as applyPresets } from "./presets-BtyFwTO4.mjs";
|
|
14
|
+
import { t as hasEvents } from "./typeGuards-C9KXUly6.mjs";
|
|
15
|
+
import { t as pluralize } from "./pluralize-DccFV9R7.mjs";
|
|
56
16
|
//#region src/core/aggregation/defineAggregation.ts
|
|
57
17
|
/**
|
|
58
18
|
* Declare a single resource aggregation. Exported configs flow into
|
|
@@ -99,13 +59,52 @@ function defineAggregation(config) {
|
|
|
99
59
|
return config;
|
|
100
60
|
}
|
|
101
61
|
//#endregion
|
|
62
|
+
//#region src/core/middlewares/fieldWrite.ts
|
|
63
|
+
/**
|
|
64
|
+
* Build a preHandler that enforces field-write permissions on the request body.
|
|
65
|
+
*
|
|
66
|
+
* Auto-CRUD's create/update routes get this for free via `BodySanitizer.sanitize()`
|
|
67
|
+
* inside `BaseController`. Custom routes (`config.routes`, presets, action endpoints)
|
|
68
|
+
* never went through that path — so a host that declared
|
|
69
|
+
* `fields: { role: fields.writableBy(['admin']) }` and a custom
|
|
70
|
+
* `POST /users/promote` happily accepted `{ role: 'admin' }` from any caller.
|
|
71
|
+
*
|
|
72
|
+
* This preHandler closes that gap. It applies ONLY the `applyFieldWritePermissions`
|
|
73
|
+
* step — no `SYSTEM_FIELDS` strip, no `fieldRules` strip — because custom-route
|
|
74
|
+
* bodies don't necessarily mirror the resource's adapter shape, and we'd
|
|
75
|
+
* generate surprising 403s by enforcing rules tied to that shape.
|
|
76
|
+
*
|
|
77
|
+
* Returns `null` when the resource has no field permissions configured — the
|
|
78
|
+
* caller should then skip wiring it into the preHandler chain.
|
|
79
|
+
*/
|
|
80
|
+
function buildFieldWritePreHandler(fieldPermissions, policy = DEFAULT_FIELD_WRITE_DENIAL_POLICY) {
|
|
81
|
+
if (!fieldPermissions || Object.keys(fieldPermissions).length === 0) return null;
|
|
82
|
+
return async (request, _reply) => {
|
|
83
|
+
const body = request.body;
|
|
84
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return;
|
|
85
|
+
const scope = request.scope ?? PUBLIC_SCOPE;
|
|
86
|
+
if (isElevated(scope)) return;
|
|
87
|
+
const { body: filtered, deniedFields } = applyFieldWritePermissions(body, fieldPermissions, resolveEffectiveRoles(getUserRoles(request.user), isMember(scope) ? scope.orgRoles : []));
|
|
88
|
+
if (deniedFields.length > 0 && policy === "reject") throw new ForbiddenError(`Not permitted to write field${deniedFields.length === 1 ? "" : "s"}: ${deniedFields.join(", ")}`);
|
|
89
|
+
request.body = filtered;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True for HTTP methods that carry a request body and should run the
|
|
94
|
+
* field-write preHandler. GET/DELETE/HEAD/OPTIONS skip enforcement.
|
|
95
|
+
*/
|
|
96
|
+
function methodCarriesBody(method) {
|
|
97
|
+
const m = method.toUpperCase();
|
|
98
|
+
return m === "POST" || m === "PUT" || m === "PATCH";
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
102
101
|
//#region src/core/createCrudRouter.ts
|
|
103
102
|
/**
|
|
104
103
|
* Mount custom routes (from presets or user-defined `routes`) on Fastify.
|
|
105
104
|
* `wrapHandler` is derived inline from `!route.raw`.
|
|
106
105
|
*/
|
|
107
106
|
function createCustomRoutes(fastify, routes, controller, options) {
|
|
108
|
-
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied } = options;
|
|
107
|
+
const { tag, resourceName, arcDecorator, rateLimitConfig, pluginMw, pipeline, routeGuards, tenantScopeMw, fieldPermissions, onFieldWriteDenied, extensions } = options;
|
|
109
108
|
for (const route of routes) {
|
|
110
109
|
const routeWithRefs = route;
|
|
111
110
|
const hasHandler = route.handler !== void 0;
|
|
@@ -178,7 +177,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
178
177
|
}
|
|
179
178
|
return result;
|
|
180
179
|
} : handler,
|
|
181
|
-
...rateLimitConfig
|
|
180
|
+
...buildRouteConfig(rateLimitConfig, extensions)
|
|
182
181
|
}, {
|
|
183
182
|
resourceName,
|
|
184
183
|
op: opName
|
|
@@ -195,7 +194,7 @@ function createCustomRoutes(fastify, routes, controller, options) {
|
|
|
195
194
|
* @param options - Router configuration
|
|
196
195
|
*/
|
|
197
196
|
function createCrudRouter(fastify, controller, options = {}) {
|
|
198
|
-
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField } = options;
|
|
197
|
+
const { tag = "Resource", schemas = {}, permissions = {}, middlewares = {}, routeGuards = [], routes: customRoutes = [], disableDefaultRoutes = false, disabledRoutes = [], resourceName = "unknown", schemaOptions, rateLimit, pipe: pipeline, fields: fieldPermissions, onFieldWriteDenied, updateMethod = DEFAULT_UPDATE_METHOD, idField, extensions } = options;
|
|
199
198
|
const rateLimitConfig = buildRateLimitConfig(rateLimit);
|
|
200
199
|
const resourceHasQueryCache = fastify.hasDecorator("queryCache") && controller && typeof controller._cacheConfig !== "undefined" && controller._cacheConfig !== void 0;
|
|
201
200
|
const pluginMw = resolveRouterPluginMw(fastify, Boolean(resourceHasQueryCache));
|
|
@@ -296,7 +295,7 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
296
295
|
}, defaultSchemas[spec.op], schemas[spec.op]),
|
|
297
296
|
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
298
297
|
handler: handlers[spec.op],
|
|
299
|
-
...rateLimitConfig
|
|
298
|
+
...buildRouteConfig(rateLimitConfig, extensions)
|
|
300
299
|
}, {
|
|
301
300
|
resourceName,
|
|
302
301
|
op: spec.op
|
|
@@ -314,7 +313,8 @@ function createCrudRouter(fastify, controller, options = {}) {
|
|
|
314
313
|
routeGuards,
|
|
315
314
|
tenantScopeMw,
|
|
316
315
|
fieldPermissions,
|
|
317
|
-
onFieldWriteDenied
|
|
316
|
+
onFieldWriteDenied,
|
|
317
|
+
extensions
|
|
318
318
|
});
|
|
319
319
|
}
|
|
320
320
|
function buildCrudHandlers(ctrl, pipeline, resourceName) {
|
|
@@ -1243,10 +1243,11 @@ function buildResourcePlugin(resource) {
|
|
|
1243
1243
|
pipe: resource.pipe,
|
|
1244
1244
|
fields: resource.fields,
|
|
1245
1245
|
onFieldWriteDenied: resource.onFieldWriteDenied,
|
|
1246
|
-
idField: resource.idField
|
|
1246
|
+
idField: resource.idField,
|
|
1247
|
+
extensions: resource.extensions
|
|
1247
1248
|
});
|
|
1248
1249
|
if (resource.actions && Object.keys(resource.actions).length > 0) {
|
|
1249
|
-
const { createActionRouter } = await import("./createActionRouter-
|
|
1250
|
+
const { createActionRouter } = await import("./createActionRouter-B_WzBFyf.mjs").then((n) => n.n);
|
|
1250
1251
|
createActionRouter(typedInstance, {
|
|
1251
1252
|
...normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, typedInstance.log),
|
|
1252
1253
|
resourceName: resource.name,
|
|
@@ -1256,11 +1257,12 @@ function buildResourcePlugin(resource) {
|
|
|
1256
1257
|
permissions: resource.permissions,
|
|
1257
1258
|
routeGuards: resource.routeGuards,
|
|
1258
1259
|
pipeline: resource.pipe,
|
|
1259
|
-
rateLimit: resource.rateLimit
|
|
1260
|
+
rateLimit: resource.rateLimit,
|
|
1261
|
+
extensions: resource.extensions
|
|
1260
1262
|
});
|
|
1261
1263
|
}
|
|
1262
1264
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
1263
|
-
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-
|
|
1265
|
+
const [{ createAggregationRouter }, { adapterSupportsAggregate, ArcAggregationConfigError }] = await Promise.all([import("./createAggregationRouter-B_G_nIjm.mjs"), import("./validate-CRcaWzp2.mjs").then((n) => n.r)]);
|
|
1264
1266
|
const repoForAgg = resource.controller?.repository ?? resource.repository;
|
|
1265
1267
|
if (!adapterSupportsAggregate(repoForAgg)) {
|
|
1266
1268
|
const undispatchable = Object.entries(resource.aggregations).filter(([, cfg]) => !cfg.materialized).map(([name]) => name);
|
|
@@ -1333,6 +1335,7 @@ var ResourceDefinition = class {
|
|
|
1333
1335
|
*/
|
|
1334
1336
|
onFieldWriteDenied;
|
|
1335
1337
|
cache;
|
|
1338
|
+
extensions;
|
|
1336
1339
|
referenceData;
|
|
1337
1340
|
/**
|
|
1338
1341
|
* Per-resource MCP opt-out. `false` keeps the resource out of every
|
|
@@ -1430,6 +1433,7 @@ var ResourceDefinition = class {
|
|
|
1430
1433
|
this.fields = config.fields;
|
|
1431
1434
|
this.onFieldWriteDenied = config.onFieldWriteDenied;
|
|
1432
1435
|
this.cache = config.cache;
|
|
1436
|
+
this.extensions = config.extensions ? Object.freeze({ ...config.extensions }) : void 0;
|
|
1433
1437
|
this.mcp = config.mcp !== false;
|
|
1434
1438
|
this.referenceData = config.referenceData === true;
|
|
1435
1439
|
this.tenantField = config.tenantField;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { t as __exportAll } from "./
|
|
2
|
-
import "./constants-
|
|
3
|
-
import { f as createError } from "./errors-
|
|
4
|
-
import {
|
|
5
|
-
import { n as buildActionPermissionMw, t as tryRegisterRoute } from "./registerRoute-
|
|
6
|
-
import {
|
|
7
|
-
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-CE6USIRi.mjs";
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
|
+
import "./constants-gaEeMlz-.mjs";
|
|
3
|
+
import { f as createError } from "./errors-DkO8Nk6-.mjs";
|
|
4
|
+
import { i as resolvePipelineSteps, n as buildActionPipelineHandler, u as sendControllerResponse } from "./actionPermissions-D7NGwcAR.mjs";
|
|
5
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, d as buildAuthMiddlewareForPermissions, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, o as buildActionPermissionMw, r as buildRouteConfig, t as tryRegisterRoute } from "./registerRoute-BdV8DDwU.mjs";
|
|
6
|
+
import { n as schemaIRToJsonSchemaBranch, t as normalizeSchemaIR } from "./schemaIR-D1MJwqmp.mjs";
|
|
8
7
|
//#region src/core/createActionRouter.ts
|
|
9
8
|
var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
10
9
|
buildActionBodySchema: () => buildActionBodySchema,
|
|
@@ -19,7 +18,7 @@ var createActionRouter_exports = /* @__PURE__ */ __exportAll({
|
|
|
19
18
|
* (keyed by `body.action` at request time).
|
|
20
19
|
*/
|
|
21
20
|
function createActionRouter(fastify, config) {
|
|
22
|
-
const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, idField = "_id", permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit, idLessActionNames = [] } = config;
|
|
21
|
+
const { tag, resourceName = tag ?? "action", actions, actionPermissions = {}, actionSchemas = {}, globalAuth, onError, fields: fieldPermissions, schemaOptions, idField = "_id", permissions: resourcePermissions, routeGuards = [], pipeline, rateLimit, idLessActionNames = [], extensions } = config;
|
|
23
22
|
const allActionNames = Object.keys(actions);
|
|
24
23
|
if (allActionNames.length === 0) {
|
|
25
24
|
fastify.log.warn("[createActionRouter] No actions defined, skipping route creation");
|
|
@@ -122,7 +121,7 @@ function createActionRouter(fastify, config) {
|
|
|
122
121
|
schema: routeSchema,
|
|
123
122
|
attachValidation: true,
|
|
124
123
|
preHandler: preHandler.length > 0 ? preHandler : void 0,
|
|
125
|
-
...rateLimitConfig
|
|
124
|
+
...buildRouteConfig(rateLimitConfig, extensions),
|
|
126
125
|
handler: async (req, reply) => {
|
|
127
126
|
const { action, ...data } = req.body;
|
|
128
127
|
const id = hasId ? req.params.id : "";
|
|
@@ -136,7 +135,8 @@ function createActionRouter(fastify, config) {
|
|
|
136
135
|
const handler = wrappedHandlers.get(action);
|
|
137
136
|
if (!handler) throw createError(400, `Invalid action '${action}'. Valid actions: ${actionSubset.join(", ")}`, { validActions: actionSubset });
|
|
138
137
|
try {
|
|
139
|
-
|
|
138
|
+
sendControllerResponse(reply, await handler(id, data, req), req);
|
|
139
|
+
return reply;
|
|
140
140
|
} catch (error) {
|
|
141
141
|
if (onError) {
|
|
142
142
|
const { statusCode, error: errorMsg, code } = onError(error, action, id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-
|
|
2
|
-
import {
|
|
3
|
-
import { n as validateAggregations } from "./validate-
|
|
4
|
-
import { t as buildAggregationHandler } from "./buildHandler-
|
|
1
|
+
import { f as createError, l as UnauthorizedError, r as ForbiddenError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
+
import { a as selectPluginMw, c as buildPreHandlerChain, f as buildArcDecorator, i as resolveRouterPluginMw, n as buildRateLimitConfig, u as buildAuthMiddleware } from "./registerRoute-BdV8DDwU.mjs";
|
|
3
|
+
import { n as validateAggregations } from "./validate-CRcaWzp2.mjs";
|
|
4
|
+
import { t as buildAggregationHandler } from "./buildHandler-B0qvAguf.mjs";
|
|
5
5
|
//#region src/core/aggregation/createAggregationRouter.ts
|
|
6
6
|
/**
|
|
7
7
|
* Register one Fastify route per aggregation. No-op when the map is
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as __exportAll } from "./
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import { n as PUBLIC_SCOPE } from "./types-
|
|
3
|
+
import { n as PUBLIC_SCOPE } from "./types-bHblZ9YN.mjs";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import qs from "qs";
|
|
6
6
|
//#region src/factory/presets.ts
|
|
@@ -74,7 +74,7 @@ const productionPreset = {
|
|
|
74
74
|
*/
|
|
75
75
|
function devLoggerConfig() {
|
|
76
76
|
try {
|
|
77
|
-
if (typeof import.meta
|
|
77
|
+
if (typeof import.meta.resolve === "function") {
|
|
78
78
|
import.meta.resolve("pino-pretty");
|
|
79
79
|
return {
|
|
80
80
|
level: "debug",
|
|
@@ -202,7 +202,7 @@ async function registerArcCore(fastify, config, trackPlugin) {
|
|
|
202
202
|
await fastify.register(arcCorePlugin, { emitEvents: config.arcPlugins?.emitEvents !== false });
|
|
203
203
|
trackPlugin("arc-core");
|
|
204
204
|
if (config.arcPlugins?.events !== false) {
|
|
205
|
-
const { default: eventPlugin } = await import("./eventPlugin-
|
|
205
|
+
const { default: eventPlugin } = await import("./eventPlugin-DJG2D7sy.mjs").then((n) => n.n);
|
|
206
206
|
const eventOpts = typeof config.arcPlugins?.events === "object" ? config.arcPlugins.events : {};
|
|
207
207
|
await fastify.register(eventPlugin, {
|
|
208
208
|
...eventOpts,
|
|
@@ -239,15 +239,15 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
|
|
|
239
239
|
trackPlugin("arc-graceful-shutdown");
|
|
240
240
|
}
|
|
241
241
|
if (config.arcPlugins?.caching) {
|
|
242
|
-
const { default: cachingPlugin } = await import("./caching-
|
|
242
|
+
const { default: cachingPlugin } = await import("./caching-_NJeDd1_.mjs").then((n) => n.r);
|
|
243
243
|
const opts = config.arcPlugins.caching === true ? {} : config.arcPlugins.caching;
|
|
244
244
|
await fastify.register(cachingPlugin, opts);
|
|
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-emHb0CuX.mjs").then((n) => n.n);
|
|
249
249
|
const opts = config.arcPlugins.queryCache === true ? {} : config.arcPlugins.queryCache;
|
|
250
|
-
const store = config.stores?.queryCache ?? new (await (import("./memory-
|
|
250
|
+
const store = config.stores?.queryCache ?? new (await (import("./memory-CLXA2VeJ.mjs").then((n) => n.n))).MemoryCacheStore();
|
|
251
251
|
await fastify.register(queryCachePlugin, {
|
|
252
252
|
store,
|
|
253
253
|
...opts
|
|
@@ -256,19 +256,19 @@ async function registerArcPlugins(fastify, config, trackPlugin, modules) {
|
|
|
256
256
|
}
|
|
257
257
|
if (config.arcPlugins?.sse) if (config.arcPlugins?.events === false) fastify.log.warn("SSE plugin requires events plugin (arcPlugins.events). SSE disabled.");
|
|
258
258
|
else {
|
|
259
|
-
const { default: ssePlugin } = await import("./sse-
|
|
259
|
+
const { default: ssePlugin } = await import("./sse-CfUjwjDk.mjs").then((n) => n.r);
|
|
260
260
|
const opts = config.arcPlugins.sse === true ? {} : config.arcPlugins.sse;
|
|
261
261
|
await fastify.register(ssePlugin, opts);
|
|
262
262
|
trackPlugin("arc-sse", opts);
|
|
263
263
|
}
|
|
264
264
|
if (config.arcPlugins?.metrics) {
|
|
265
|
-
const { default: metricsPlugin } = await import("./metrics-
|
|
265
|
+
const { default: metricsPlugin } = await import("./metrics-fdxI5mn_.mjs").then((n) => n.r);
|
|
266
266
|
const opts = config.arcPlugins.metrics === true ? {} : config.arcPlugins.metrics;
|
|
267
267
|
await fastify.register(metricsPlugin, opts);
|
|
268
268
|
trackPlugin("arc-metrics", opts);
|
|
269
269
|
}
|
|
270
270
|
if (config.arcPlugins?.versioning) {
|
|
271
|
-
const { default: versioningPlugin } = await import("./versioning-
|
|
271
|
+
const { default: versioningPlugin } = await import("./versioning-Dk-71iIz.mjs").then((n) => n.r);
|
|
272
272
|
await fastify.register(versioningPlugin, config.arcPlugins.versioning);
|
|
273
273
|
trackPlugin("arc-versioning", config.arcPlugins.versioning);
|
|
274
274
|
}
|
|
@@ -337,7 +337,7 @@ async function registerAuth(fastify, config, trackPlugin) {
|
|
|
337
337
|
*/
|
|
338
338
|
async function registerElevation(fastify, config, trackPlugin) {
|
|
339
339
|
if (!config.elevation) return;
|
|
340
|
-
const { elevationPlugin } = await import("./elevation-
|
|
340
|
+
const { elevationPlugin } = await import("./elevation-COpYKt2h.mjs").then((n) => n.r);
|
|
341
341
|
await fastify.register(elevationPlugin, config.elevation);
|
|
342
342
|
trackPlugin("arc-elevation", config.elevation);
|
|
343
343
|
fastify.log.debug("Elevation plugin enabled");
|
|
@@ -347,7 +347,7 @@ async function registerElevation(fastify, config, trackPlugin) {
|
|
|
347
347
|
*/
|
|
348
348
|
async function registerErrorHandler(fastify, config, trackPlugin) {
|
|
349
349
|
if (config.errorHandler === false) return;
|
|
350
|
-
const { errorHandlerPlugin } = await import("./errorHandler-
|
|
350
|
+
const { errorHandlerPlugin } = await import("./errorHandler-Dxf6P_kV.mjs").then((n) => n.r);
|
|
351
351
|
const errorOpts = typeof config.errorHandler === "object" ? config.errorHandler : { includeStack: config.preset !== "production" };
|
|
352
352
|
await fastify.register(errorHandlerPlugin, errorOpts);
|
|
353
353
|
trackPlugin("arc-error-handler", errorOpts);
|
|
@@ -448,7 +448,7 @@ async function registerResources(fastify, config) {
|
|
|
448
448
|
let discoveryPath;
|
|
449
449
|
let discoveryYieldedZero = false;
|
|
450
450
|
if (resolvedResources === void 0 && config.resourceDir) {
|
|
451
|
-
const { loadResources } = await import("./loadResources-
|
|
451
|
+
const { loadResources } = await import("./loadResources-B8UmpITt.mjs").then((n) => n.n);
|
|
452
452
|
const { resolve, dirname } = await import("node:path");
|
|
453
453
|
const { fileURLToPath } = await import("node:url");
|
|
454
454
|
const rawDir = config.resourceDir;
|
|
@@ -896,7 +896,7 @@ async function createApp(options) {
|
|
|
896
896
|
await registerErrorHandler(fastify, config, trackPlugin);
|
|
897
897
|
await registerResources(fastify, config);
|
|
898
898
|
if (config.replyHelpers) {
|
|
899
|
-
const { replyHelpersPlugin } = await import("./replyHelpers-
|
|
899
|
+
const { replyHelpersPlugin } = await import("./replyHelpers-67tRawQZ.mjs").then((n) => n.n);
|
|
900
900
|
await fastify.register(replyHelpersPlugin);
|
|
901
901
|
}
|
|
902
902
|
if (config.serializeBigInt) {
|
package/dist/docs/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { p as RegistryEntry } from "../index-
|
|
2
|
-
import { t as ExternalOpenApiPaths } from "../externalPaths-
|
|
1
|
+
import { p as RegistryEntry } from "../index-CUTG2Cuo.mjs";
|
|
2
|
+
import { t as ExternalOpenApiPaths } from "../externalPaths-BE6wFpYv.mjs";
|
|
3
3
|
import { FastifyPluginAsync } from "fastify";
|
|
4
4
|
|
|
5
5
|
//#region src/docs/openapi/types.d.ts
|
package/dist/docs/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as getUserRoles } from "../types-
|
|
2
|
-
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-
|
|
1
|
+
import { t as getUserRoles } from "../types-XTApa8b6.mjs";
|
|
2
|
+
import { n as openApiPlugin, r as openapi_default, t as buildOpenApiSpec } from "../openapi-DZ-5a5Op.mjs";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
4
|
//#region src/docs/scalar.ts
|
|
5
5
|
const scalarPlugin = async (fastify, opts = {}) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as __exportAll } from "./
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
|
|
2
2
|
import { arcLog } from "./logger/index.mjs";
|
|
3
|
-
import { t as getUserRoles } from "./types-
|
|
3
|
+
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
4
4
|
import fp from "fastify-plugin";
|
|
5
5
|
//#region src/scope/elevation.ts
|
|
6
6
|
var elevation_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -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 };
|