@classytic/arc 2.22.0 → 2.25.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-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +264 -260
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +168 -93
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +21 -6
- package/dist/factory/index.mjs +58 -18
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +32 -20
- package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +68 -5
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +149 -22
- package/dist/integrations/webhooks.mjs +186 -42
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +4 -3
- package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
- package/dist/plugins/index.d.mts +7 -78
- package/dist/plugins/index.mjs +14 -97
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
- package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- 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 +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/realtime-Cuw_deA-.mjs +375 -0
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/scope-DfaPntpo.mjs +36 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +90 -57
- package/dist/utils/index.mjs +7 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -453
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/skills/arc/references/production.md +734 -696
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/scope-k15RXfcq.mjs +0 -64
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { n as requireSingleHeaderValue } from "./headers-BvlQSDMB.mjs";
|
|
3
|
+
import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-CSyCxl2V.mjs";
|
|
4
|
+
import { t as BaseController } from "./BaseController--M8EMzt0.mjs";
|
|
5
|
+
import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
|
|
5
6
|
import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import { n as validateAggregations } from "./validate-
|
|
8
|
-
import { n as executeAggregation } from "./buildHandler-
|
|
7
|
+
import { r as resolvePipelineSteps, u as buildRequestScopeProjection } from "./pipeline-BN5G05rd.mjs";
|
|
8
|
+
import { n as validateAggregations } from "./validate-Bk44F2yT.mjs";
|
|
9
|
+
import { n as executeAggregation } from "./buildHandler-Dtwd8fdz.mjs";
|
|
9
10
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
11
|
+
import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
|
|
12
|
+
import { t as createIdempotencyResult } from "./interface-n5hpKg1d.mjs";
|
|
13
|
+
import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
|
|
12
14
|
import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
|
|
15
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
16
|
import fp from "fastify-plugin";
|
|
14
17
|
import { z } from "zod";
|
|
15
18
|
//#region src/integrations/mcp/createMcpServer.ts
|
|
@@ -160,6 +163,13 @@ function registerPrompt(server, prompt) {
|
|
|
160
163
|
//#endregion
|
|
161
164
|
//#region src/integrations/mcp/buildRequestContext.ts
|
|
162
165
|
/**
|
|
166
|
+
* @classytic/arc — MCP → IRequestContext Bridge
|
|
167
|
+
*
|
|
168
|
+
* Builds an IRequestContext from MCP tool input + session identity.
|
|
169
|
+
* Ensures MCP tool handlers go through the same BaseController pipeline
|
|
170
|
+
* as REST requests (AccessControl, BodySanitizer, QueryResolver, HookSystem).
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
163
173
|
* Build an IRequestContext from MCP tool input and session auth.
|
|
164
174
|
*
|
|
165
175
|
* | Operation | params | query | body |
|
|
@@ -177,7 +187,7 @@ function registerPrompt(server, prompt) {
|
|
|
177
187
|
* the session-derived scope is `public` (i.e. MCP called with `auth: false`).
|
|
178
188
|
* An authenticated session scope is never overwritten.
|
|
179
189
|
*/
|
|
180
|
-
function buildRequestContext(input, auth, operation, policyFilters, scopeOverride) {
|
|
190
|
+
function buildRequestContext(input, auth, operation, policyFilters, scopeOverride, extras) {
|
|
181
191
|
const sessionScope = buildScope(auth);
|
|
182
192
|
const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
|
|
183
193
|
const base = {
|
|
@@ -188,10 +198,13 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
|
|
|
188
198
|
} : null,
|
|
189
199
|
headers: {},
|
|
190
200
|
context: {},
|
|
201
|
+
scope: buildRequestScopeProjection(scope),
|
|
191
202
|
metadata: {
|
|
192
203
|
_scope: scope,
|
|
193
|
-
_policyFilters: policyFilters ?? {}
|
|
194
|
-
|
|
204
|
+
_policyFilters: policyFilters ?? {},
|
|
205
|
+
...extras?.arc ? { arc: extras.arc } : {}
|
|
206
|
+
},
|
|
207
|
+
...extras?.server ? { server: extras.server } : {}
|
|
195
208
|
};
|
|
196
209
|
switch (operation) {
|
|
197
210
|
case "list": return {
|
|
@@ -375,6 +388,42 @@ async function evaluatePermission(check, session, resource, action, input) {
|
|
|
375
388
|
}));
|
|
376
389
|
}
|
|
377
390
|
/**
|
|
391
|
+
* Apply field-level READ permissions to an MCP tool payload — the same
|
|
392
|
+
* masking `sendControllerResponse` applies on the HTTP wire and the
|
|
393
|
+
* realtime plugin applies per frame. Without this, a resource declaring
|
|
394
|
+
* `fields: { salary: fields.visibleTo(['admin']) }` masked salary over
|
|
395
|
+
* REST and SSE but leaked it verbatim through every MCP tool.
|
|
396
|
+
*
|
|
397
|
+
* Mirrors the HTTP adapter's semantics exactly:
|
|
398
|
+
* - Elevated scope (platform admin) bypasses masking — consistent with
|
|
399
|
+
* `requireOrgRole()` and `BodySanitizer` bypass logic.
|
|
400
|
+
* - Effective roles = session roles ∪ org roles (member scope only),
|
|
401
|
+
* via the shared `resolveEffectiveRoles`.
|
|
402
|
+
* - `scopeOverride` (a `PermissionResult.scope`) follows the same
|
|
403
|
+
* non-downgrade rule as `buildRequestContext`: honored only when the
|
|
404
|
+
* session-derived scope is `public`.
|
|
405
|
+
*
|
|
406
|
+
* Handles the three payload shapes MCP tools emit: a single record, an
|
|
407
|
+
* array of records, and a paginated result object carrying an inner
|
|
408
|
+
* `data: T[]`. Scalars and `null` pass through untouched.
|
|
409
|
+
*/
|
|
410
|
+
function applyMcpReadMasking(data, options) {
|
|
411
|
+
const { fields, session, scopeOverride } = options;
|
|
412
|
+
if (!fields || data === null || typeof data !== "object") return data;
|
|
413
|
+
const sessionScope = buildScope(session);
|
|
414
|
+
const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
|
|
415
|
+
if (isElevated(scope)) return data;
|
|
416
|
+
const roles = resolveEffectiveRoles(session?.roles ?? [], isMember(scope) ? scope.orgRoles : []);
|
|
417
|
+
const maskItem = (item) => item !== null && typeof item === "object" && !Array.isArray(item) ? applyFieldReadPermissions(item, fields, roles) : item;
|
|
418
|
+
if (Array.isArray(data)) return data.map(maskItem);
|
|
419
|
+
const inner = data.data;
|
|
420
|
+
if (Array.isArray(inner)) return {
|
|
421
|
+
...data,
|
|
422
|
+
data: inner.map(maskItem)
|
|
423
|
+
};
|
|
424
|
+
return maskItem(data);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
378
427
|
* Convert a controller response envelope into an MCP `CallToolResult`.
|
|
379
428
|
* Carries `meta` into the serialized payload so consumers see pagination
|
|
380
429
|
* totals, stripped-field arrays, etc.
|
|
@@ -476,6 +525,78 @@ function createMcpController(resource) {
|
|
|
476
525
|
//#endregion
|
|
477
526
|
//#region src/integrations/mcp/invokeController.ts
|
|
478
527
|
/**
|
|
528
|
+
* @classytic/arc — Non-HTTP controller invocation for MCP / jobs / workflows.
|
|
529
|
+
*
|
|
530
|
+
* Internal callers (MCP tools, scheduled jobs, workflow steps) need to invoke
|
|
531
|
+
* controller methods without going through Fastify's preHandler chain.
|
|
532
|
+
* Before this module landed, every call site reinvented the same shim:
|
|
533
|
+
* fabricate `params` / `body` / `headers`, project the session into a
|
|
534
|
+
* `RequestScope`, run the permission check by hand, translate thrown errors
|
|
535
|
+
* to the MCP `CallToolResult` shape. The drift across those copies is what
|
|
536
|
+
* the mentora review flagged as the "buildCtx synthetic-context hack."
|
|
537
|
+
*
|
|
538
|
+
* Two public surfaces, layered so callers reach for the smallest piece that
|
|
539
|
+
* does the job:
|
|
540
|
+
*
|
|
541
|
+
* - {@link invokeController} full pipeline: permission → context →
|
|
542
|
+
* dispatch → canonical envelope/error
|
|
543
|
+
* conversion. Returns `CallToolResult`.
|
|
544
|
+
* - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
|
|
545
|
+
* async function into a `ToolDefinition`
|
|
546
|
+
* handler with canonical error shape. Use
|
|
547
|
+
* when you want to compose multiple
|
|
548
|
+
* controller calls or weave in custom logic
|
|
549
|
+
* between them.
|
|
550
|
+
*
|
|
551
|
+
* Both honor the same {@link buildRequestContext} primitive — re-exported
|
|
552
|
+
* from this module so authors who want the synthetic context as data (e.g.
|
|
553
|
+
* to pass into `executePipeline`) can grab it without depending on an
|
|
554
|
+
* internal subpath.
|
|
555
|
+
*/
|
|
556
|
+
/**
|
|
557
|
+
* Assemble the {@link McpContextExtras} for a controller invocation from
|
|
558
|
+
* resource-scoped metadata + app-level wiring. Mirrors what
|
|
559
|
+
* `buildArcDecorator` (metadata.arc) and `createRequestContext` (server
|
|
560
|
+
* accessor) assemble on the HTTP path.
|
|
561
|
+
*/
|
|
562
|
+
function buildContextExtras(options) {
|
|
563
|
+
const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
|
|
564
|
+
if (!wiring) return void 0;
|
|
565
|
+
return {
|
|
566
|
+
arc: {
|
|
567
|
+
resourceName,
|
|
568
|
+
schemaOptions,
|
|
569
|
+
permissions,
|
|
570
|
+
hooks: wiring.hooks,
|
|
571
|
+
events: wiring.events,
|
|
572
|
+
fields,
|
|
573
|
+
idField
|
|
574
|
+
},
|
|
575
|
+
server: {
|
|
576
|
+
events: wiring.events,
|
|
577
|
+
audit: wiring.audit,
|
|
578
|
+
log: wiring.log
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
|
|
584
|
+
* Scopes an idempotency key to (resource, action, input, caller) so a
|
|
585
|
+
* reused key with DIFFERENT input is a fresh execution, matching the HTTP
|
|
586
|
+
* plugin's method+url+bodyHash+user fingerprint semantics.
|
|
587
|
+
*/
|
|
588
|
+
function invocationFingerprint(resourceName, actionName, input, session) {
|
|
589
|
+
const sortKeys = (value) => {
|
|
590
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
591
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
592
|
+
return value;
|
|
593
|
+
};
|
|
594
|
+
return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
|
|
595
|
+
}
|
|
596
|
+
/** TTLs matching the HTTP idempotency plugin's defaults. */
|
|
597
|
+
const MCP_IDEMPOTENCY_TTL_MS = 1440 * 60 * 1e3;
|
|
598
|
+
const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
|
|
599
|
+
/**
|
|
479
600
|
* Invoke a controller method through the MCP synthetic-context path.
|
|
480
601
|
*
|
|
481
602
|
* Runs the same chain CRUD MCP tools run:
|
|
@@ -508,7 +629,7 @@ function createMcpController(resource) {
|
|
|
508
629
|
* checks compose.
|
|
509
630
|
*/
|
|
510
631
|
async function invokeController(controller, op, input, options) {
|
|
511
|
-
const { session, resourceName, actionName = op, permissions, methodName = op } = options;
|
|
632
|
+
const { session, resourceName, actionName = op, permissions, methodName = op, fields, wiring, idempotencyKey } = options;
|
|
512
633
|
const ctrl = controller;
|
|
513
634
|
try {
|
|
514
635
|
const method = ctrl[methodName];
|
|
@@ -524,7 +645,39 @@ async function invokeController(controller, op, input, options) {
|
|
|
524
645
|
reason: permResult.reason,
|
|
525
646
|
session
|
|
526
647
|
});
|
|
527
|
-
|
|
648
|
+
const dispatch = async () => {
|
|
649
|
+
const reqCtx = buildRequestContext(input, session, op, permResult?.filters, permResult?.scope, buildContextExtras(options));
|
|
650
|
+
const response = await method(reqCtx);
|
|
651
|
+
const data = applyMcpReadMasking(response.data, {
|
|
652
|
+
fields,
|
|
653
|
+
session,
|
|
654
|
+
scopeOverride: permResult?.scope
|
|
655
|
+
});
|
|
656
|
+
return toCallToolResult(data === response.data ? response : {
|
|
657
|
+
...response,
|
|
658
|
+
data
|
|
659
|
+
});
|
|
660
|
+
};
|
|
661
|
+
const store = wiring?.idempotencyStore;
|
|
662
|
+
if (idempotencyKey && store) {
|
|
663
|
+
const fullKey = `${idempotencyKey}:${invocationFingerprint(resourceName, actionName, input, session)}`;
|
|
664
|
+
const cached = await store.get(fullKey);
|
|
665
|
+
if (cached) return cached.body;
|
|
666
|
+
const lockId = randomUUID();
|
|
667
|
+
if (!await store.tryLock(fullKey, lockId, MCP_IDEMPOTENCY_LOCK_MS)) return toCallToolError({
|
|
668
|
+
code: "arc.idempotency_conflict",
|
|
669
|
+
message: "A call with this idempotency key is already in progress",
|
|
670
|
+
status: 409
|
|
671
|
+
});
|
|
672
|
+
try {
|
|
673
|
+
const result = await dispatch();
|
|
674
|
+
if (!result.isError) await store.set(fullKey, createIdempotencyResult(200, result, {}, MCP_IDEMPOTENCY_TTL_MS));
|
|
675
|
+
return result;
|
|
676
|
+
} finally {
|
|
677
|
+
await store.unlock(fullKey, lockId);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return await dispatch();
|
|
528
681
|
} catch (err) {
|
|
529
682
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
530
683
|
}
|
|
@@ -605,13 +758,33 @@ const CRUD_ANNOTATIONS = {
|
|
|
605
758
|
* build + envelope/error conversion all live in one place — this factory
|
|
606
759
|
* only resolves the op-specific permission and threads it through.
|
|
607
760
|
*/
|
|
608
|
-
|
|
761
|
+
/** Ops where an idempotency key is meaningful (mutations). */
|
|
762
|
+
const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
|
|
763
|
+
"create",
|
|
764
|
+
"update",
|
|
765
|
+
"delete"
|
|
766
|
+
]);
|
|
767
|
+
function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
|
|
609
768
|
const permission = permissions?.[op];
|
|
610
|
-
return (input, ctx) =>
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
769
|
+
return (input, ctx) => {
|
|
770
|
+
let idempotencyKey;
|
|
771
|
+
let payload = input;
|
|
772
|
+
if ("_idempotencyKey" in input) {
|
|
773
|
+
const { _idempotencyKey, ...rest } = input;
|
|
774
|
+
payload = rest;
|
|
775
|
+
if (MUTATING_CRUD_OPS.has(op) && typeof _idempotencyKey === "string" && _idempotencyKey) idempotencyKey = _idempotencyKey;
|
|
776
|
+
}
|
|
777
|
+
return invokeController(controller, op, payload, {
|
|
778
|
+
session: ctx.session,
|
|
779
|
+
resourceName,
|
|
780
|
+
permissions: permission,
|
|
781
|
+
fields,
|
|
782
|
+
wiring: extras?.wiring,
|
|
783
|
+
schemaOptions: extras?.schemaOptions,
|
|
784
|
+
idField: extras?.idField,
|
|
785
|
+
idempotencyKey
|
|
786
|
+
});
|
|
787
|
+
};
|
|
615
788
|
}
|
|
616
789
|
/**
|
|
617
790
|
* Default description for a CRUD tool. Enriches list descriptions with the
|
|
@@ -1008,7 +1181,24 @@ function convertActionSchemaToZod(raw) {
|
|
|
1008
1181
|
* in the 2.10.8 review: REST and MCP action tools share identical
|
|
1009
1182
|
* context-building machinery.
|
|
1010
1183
|
*/
|
|
1011
|
-
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
|
|
1184
|
+
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
|
|
1185
|
+
const wiring = extras?.wiring;
|
|
1186
|
+
const contextExtras = wiring ? {
|
|
1187
|
+
arc: {
|
|
1188
|
+
resourceName,
|
|
1189
|
+
schemaOptions: extras?.schemaOptions,
|
|
1190
|
+
permissions,
|
|
1191
|
+
hooks: wiring.hooks,
|
|
1192
|
+
events: wiring.events,
|
|
1193
|
+
fields,
|
|
1194
|
+
idField: extras?.idField
|
|
1195
|
+
},
|
|
1196
|
+
server: {
|
|
1197
|
+
events: wiring.events,
|
|
1198
|
+
audit: wiring.audit,
|
|
1199
|
+
log: wiring.log
|
|
1200
|
+
}
|
|
1201
|
+
} : void 0;
|
|
1012
1202
|
const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
|
|
1013
1203
|
const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
|
|
1014
1204
|
return async (input, ctx) => {
|
|
@@ -1036,11 +1226,15 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1036
1226
|
const reqCtx = buildRequestContext({
|
|
1037
1227
|
...input,
|
|
1038
1228
|
action: actionName
|
|
1039
|
-
}, session, "action", permResult?.filters, permResult?.scope);
|
|
1229
|
+
}, session, "action", permResult?.filters, permResult?.scope, contextExtras);
|
|
1040
1230
|
const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
|
|
1041
1231
|
const { id: _discardId, ...data } = input;
|
|
1042
1232
|
try {
|
|
1043
|
-
return toCallToolSuccess(await handler(id, data, reqCtx)
|
|
1233
|
+
return toCallToolSuccess(applyMcpReadMasking(await handler(id, data, reqCtx), {
|
|
1234
|
+
fields,
|
|
1235
|
+
session,
|
|
1236
|
+
scopeOverride: permResult?.scope
|
|
1237
|
+
}));
|
|
1044
1238
|
} catch (err) {
|
|
1045
1239
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1046
1240
|
}
|
|
@@ -1055,7 +1249,7 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1055
1249
|
* every aggregation opts out via `mcp: false`.
|
|
1056
1250
|
*/
|
|
1057
1251
|
function buildAggregationTools(args) {
|
|
1058
|
-
const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix } = args;
|
|
1252
|
+
const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix, fields } = args;
|
|
1059
1253
|
if (!aggregations || Object.keys(aggregations).length === 0) return [];
|
|
1060
1254
|
const normalized = validateAggregations(resourceName, aggregations, schemaOptions);
|
|
1061
1255
|
const tools = [];
|
|
@@ -1079,7 +1273,8 @@ function buildAggregationTools(args) {
|
|
|
1079
1273
|
permissions: config.permissions,
|
|
1080
1274
|
resourceName,
|
|
1081
1275
|
repo,
|
|
1082
|
-
buildOptionsFromSession
|
|
1276
|
+
buildOptionsFromSession,
|
|
1277
|
+
fields
|
|
1083
1278
|
})
|
|
1084
1279
|
});
|
|
1085
1280
|
}
|
|
@@ -1121,7 +1316,7 @@ function buildAggregationInputSchema(norm) {
|
|
|
1121
1316
|
return shape;
|
|
1122
1317
|
}
|
|
1123
1318
|
function createAggregationToolHandler(args) {
|
|
1124
|
-
const { normalized, permissions, resourceName, repo, buildOptionsFromSession } = args;
|
|
1319
|
+
const { normalized, permissions, resourceName, repo, buildOptionsFromSession, fields } = args;
|
|
1125
1320
|
return async (input, ctx) => {
|
|
1126
1321
|
const permResult = await evaluatePermission(permissions, ctx.session, resourceName, `aggregation:${normalized.name}`, input);
|
|
1127
1322
|
if (permResult && !permResult.granted) return permissionDeniedResult({
|
|
@@ -1140,7 +1335,21 @@ function createAggregationToolHandler(args) {
|
|
|
1140
1335
|
query: filterInput,
|
|
1141
1336
|
tenantOptions
|
|
1142
1337
|
});
|
|
1143
|
-
if (result.status === 200)
|
|
1338
|
+
if (result.status === 200) {
|
|
1339
|
+
const body = result.body;
|
|
1340
|
+
if (fields && Array.isArray(body?.rows)) {
|
|
1341
|
+
const rows = applyMcpReadMasking(body.rows, {
|
|
1342
|
+
fields,
|
|
1343
|
+
session: ctx.session,
|
|
1344
|
+
scopeOverride: permResult?.scope
|
|
1345
|
+
});
|
|
1346
|
+
return toCallToolSuccess({
|
|
1347
|
+
...body,
|
|
1348
|
+
rows
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
return toCallToolSuccess(result.body);
|
|
1352
|
+
}
|
|
1144
1353
|
return toCallToolError(result.body);
|
|
1145
1354
|
};
|
|
1146
1355
|
}
|
|
@@ -1480,8 +1689,24 @@ function operationKindForRoute(method, hasId) {
|
|
|
1480
1689
|
function createCustomRouteHandler(route, controller, hasId, options) {
|
|
1481
1690
|
const ctrl = controller;
|
|
1482
1691
|
const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
|
|
1483
|
-
const { resourceName, operationName, permissions, pipeline } = options;
|
|
1692
|
+
const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
|
|
1484
1693
|
const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
|
|
1694
|
+
const contextExtras = wiring ? {
|
|
1695
|
+
arc: {
|
|
1696
|
+
resourceName,
|
|
1697
|
+
schemaOptions: options.schemaOptions,
|
|
1698
|
+
permissions,
|
|
1699
|
+
hooks: wiring.hooks,
|
|
1700
|
+
events: wiring.events,
|
|
1701
|
+
fields,
|
|
1702
|
+
idField: options.idField
|
|
1703
|
+
},
|
|
1704
|
+
server: {
|
|
1705
|
+
events: wiring.events,
|
|
1706
|
+
audit: wiring.audit,
|
|
1707
|
+
log: wiring.log
|
|
1708
|
+
}
|
|
1709
|
+
} : void 0;
|
|
1485
1710
|
return async (input, _ctx) => {
|
|
1486
1711
|
const session = _ctx.session;
|
|
1487
1712
|
const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
|
|
@@ -1492,7 +1717,18 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1492
1717
|
session
|
|
1493
1718
|
});
|
|
1494
1719
|
try {
|
|
1495
|
-
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
|
|
1720
|
+
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope, contextExtras);
|
|
1721
|
+
const emit = (envelope) => {
|
|
1722
|
+
const data = applyMcpReadMasking(envelope.data, {
|
|
1723
|
+
fields,
|
|
1724
|
+
session,
|
|
1725
|
+
scopeOverride: permResult?.scope
|
|
1726
|
+
});
|
|
1727
|
+
return toCallToolResult(data === envelope.data ? envelope : {
|
|
1728
|
+
...envelope,
|
|
1729
|
+
data
|
|
1730
|
+
});
|
|
1731
|
+
};
|
|
1496
1732
|
if (typeof route.handler === "function") {
|
|
1497
1733
|
const fn = route.handler;
|
|
1498
1734
|
if (pipelineSteps.length > 0) {
|
|
@@ -1501,13 +1737,13 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1501
1737
|
resource: resourceName,
|
|
1502
1738
|
operation: operationName
|
|
1503
1739
|
};
|
|
1504
|
-
return
|
|
1740
|
+
return emit(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
|
|
1505
1741
|
const raw = await fn(ctx);
|
|
1506
1742
|
return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
|
|
1507
1743
|
}, operationName));
|
|
1508
1744
|
}
|
|
1509
1745
|
const out = await fn(reqCtx);
|
|
1510
|
-
return
|
|
1746
|
+
return emit(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
|
|
1511
1747
|
}
|
|
1512
1748
|
if (!ctrl) return {
|
|
1513
1749
|
content: [{
|
|
@@ -1524,7 +1760,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1524
1760
|
}],
|
|
1525
1761
|
isError: true
|
|
1526
1762
|
};
|
|
1527
|
-
return
|
|
1763
|
+
return emit(await method(reqCtx));
|
|
1528
1764
|
} catch (err) {
|
|
1529
1765
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1530
1766
|
}
|
|
@@ -1598,6 +1834,7 @@ function resourceToTools(resource, config = {}) {
|
|
|
1598
1834
|
const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
|
|
1599
1835
|
const tools = [];
|
|
1600
1836
|
const prefix = config.toolNamePrefix;
|
|
1837
|
+
const wiring = config.wiring;
|
|
1601
1838
|
if (controller) {
|
|
1602
1839
|
let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
|
|
1603
1840
|
if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
|
|
@@ -1608,6 +1845,15 @@ function resourceToTools(resource, config = {}) {
|
|
|
1608
1845
|
allowedOperators,
|
|
1609
1846
|
sortableFields
|
|
1610
1847
|
});
|
|
1848
|
+
const inputSchema = buildInputSchema(op, fieldRules, {
|
|
1849
|
+
hiddenFields,
|
|
1850
|
+
readonlyFields,
|
|
1851
|
+
extraHideFields: config.hideFields,
|
|
1852
|
+
filterableFields,
|
|
1853
|
+
allowedOperators,
|
|
1854
|
+
adapterBodies
|
|
1855
|
+
});
|
|
1856
|
+
if (wiring?.idempotencyStore && op !== "list" && op !== "get") inputSchema._idempotencyKey = z.string().optional().describe("Optional idempotency key — retrying with the same key and input replays the first successful result instead of re-executing.");
|
|
1611
1857
|
tools.push({
|
|
1612
1858
|
name,
|
|
1613
1859
|
description: resolveCrudDescription(config.descriptions?.[op], {
|
|
@@ -1620,15 +1866,12 @@ function resourceToTools(resource, config = {}) {
|
|
|
1620
1866
|
sortableFields
|
|
1621
1867
|
}),
|
|
1622
1868
|
annotations: CRUD_ANNOTATIONS[op],
|
|
1623
|
-
inputSchema
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
allowedOperators,
|
|
1629
|
-
adapterBodies
|
|
1869
|
+
inputSchema,
|
|
1870
|
+
handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
|
|
1871
|
+
wiring,
|
|
1872
|
+
schemaOptions: resource.schemaOptions,
|
|
1873
|
+
idField: resource.idField
|
|
1630
1874
|
}),
|
|
1631
|
-
handler: createCrudHandler(op, controller, resource.name, resource.permissions),
|
|
1632
1875
|
source: `crud:${resource.name}:${op}`
|
|
1633
1876
|
});
|
|
1634
1877
|
}
|
|
@@ -1680,7 +1923,11 @@ function resourceToTools(resource, config = {}) {
|
|
|
1680
1923
|
resourceName: resource.name,
|
|
1681
1924
|
operationName: opName,
|
|
1682
1925
|
permissions: route.permissions,
|
|
1683
|
-
pipeline: resource.pipe
|
|
1926
|
+
pipeline: resource.pipe,
|
|
1927
|
+
fields: resource.fields,
|
|
1928
|
+
wiring,
|
|
1929
|
+
schemaOptions: resource.schemaOptions,
|
|
1930
|
+
idField: resource.idField
|
|
1684
1931
|
})
|
|
1685
1932
|
});
|
|
1686
1933
|
}
|
|
@@ -1711,7 +1958,11 @@ function resourceToTools(resource, config = {}) {
|
|
|
1711
1958
|
annotations,
|
|
1712
1959
|
inputSchema: inputShape,
|
|
1713
1960
|
source: `action:${resource.name}:${actionName}`,
|
|
1714
|
-
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess
|
|
1961
|
+
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
|
|
1962
|
+
wiring,
|
|
1963
|
+
schemaOptions: resource.schemaOptions,
|
|
1964
|
+
idField: resource.idField
|
|
1965
|
+
})
|
|
1715
1966
|
});
|
|
1716
1967
|
}
|
|
1717
1968
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
@@ -1738,7 +1989,8 @@ function resourceToTools(resource, config = {}) {
|
|
|
1738
1989
|
schemaOptions: resource.schemaOptions,
|
|
1739
1990
|
repo: repoForAgg,
|
|
1740
1991
|
buildOptionsFromSession,
|
|
1741
|
-
prefix
|
|
1992
|
+
prefix,
|
|
1993
|
+
fields: resource.fields
|
|
1742
1994
|
}));
|
|
1743
1995
|
}
|
|
1744
1996
|
return tools;
|
|
@@ -1948,10 +2200,29 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
1948
2200
|
include: options.include,
|
|
1949
2201
|
exclude: options.exclude
|
|
1950
2202
|
});
|
|
2203
|
+
const fastifyAny = fastify;
|
|
2204
|
+
const wiring = {
|
|
2205
|
+
get hooks() {
|
|
2206
|
+
return fastifyAny.arc?.hooks;
|
|
2207
|
+
},
|
|
2208
|
+
get events() {
|
|
2209
|
+
return fastifyAny.events;
|
|
2210
|
+
},
|
|
2211
|
+
get audit() {
|
|
2212
|
+
return fastifyAny.audit;
|
|
2213
|
+
},
|
|
2214
|
+
get log() {
|
|
2215
|
+
return fastify.log;
|
|
2216
|
+
},
|
|
2217
|
+
get idempotencyStore() {
|
|
2218
|
+
return fastifyAny.idempotency?.store;
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
1951
2221
|
const overrides = options.overrides ?? {};
|
|
1952
2222
|
const allTools = enabledResources.flatMap((r) => {
|
|
1953
2223
|
const resOverrides = overrides[r.name] ?? {};
|
|
1954
2224
|
return resourceToTools(r, {
|
|
2225
|
+
wiring,
|
|
1955
2226
|
...resOverrides,
|
|
1956
2227
|
toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
|
|
1957
2228
|
});
|
|
@@ -2113,7 +2384,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2113
2384
|
}
|
|
2114
2385
|
});
|
|
2115
2386
|
}
|
|
2116
|
-
const sessionId = request.headers
|
|
2387
|
+
const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
|
|
2117
2388
|
if (sessionId) {
|
|
2118
2389
|
const entry = cache.get(sessionId);
|
|
2119
2390
|
if (entry) {
|
|
@@ -2148,7 +2419,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2148
2419
|
await transport.handleRequest(request.raw, reply.raw, request.body);
|
|
2149
2420
|
});
|
|
2150
2421
|
fastify.get(prefix, async (request, reply) => {
|
|
2151
|
-
const sessionId = request.headers
|
|
2422
|
+
const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
|
|
2152
2423
|
if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
|
|
2153
2424
|
const entry = cache.get(sessionId);
|
|
2154
2425
|
if (!entry) return reply.code(403).send({ error: "Unauthorized" });
|
|
@@ -2163,7 +2434,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2163
2434
|
await entry.transport.handleRequest(request.raw, reply.raw);
|
|
2164
2435
|
});
|
|
2165
2436
|
fastify.delete(prefix, async (request, reply) => {
|
|
2166
|
-
const sessionId = request.headers
|
|
2437
|
+
const sessionId = requireSingleHeaderValue(request.headers, "mcp-session-id");
|
|
2167
2438
|
if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
|
|
2168
2439
|
const entry = cache.get(sessionId);
|
|
2169
2440
|
if (!entry) return reply.code(204).send();
|
|
@@ -76,6 +76,35 @@ var MemoryCacheStore = class {
|
|
|
76
76
|
const entry = this.cache.get(key);
|
|
77
77
|
if (entry) this.removeEntry(key, entry);
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Atomic increment (canonical `CacheAdapter` signature: key, by, ttl) —
|
|
81
|
+
* synchronous on purpose: no `await` between read and write means
|
|
82
|
+
* concurrent bumps on the event loop can't interleave, which is the whole
|
|
83
|
+
* point of the `CacheStore.increment` contract.
|
|
84
|
+
*
|
|
85
|
+
* TTL semantics follow the repo-core contract: `ttlSeconds` applies only
|
|
86
|
+
* when the key is CREATED — an existing counter keeps its original expiry
|
|
87
|
+
* (Redis `EXPIRE ... NX` semantics), so every conforming adapter ages a
|
|
88
|
+
* counter identically.
|
|
89
|
+
*/
|
|
90
|
+
increment(key, by = 1, ttlSeconds) {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const entry = this.cache.get(key);
|
|
93
|
+
const live = entry && entry.expiresAt > now;
|
|
94
|
+
const next = (live && typeof entry.value === "number" ? entry.value : 0) + by;
|
|
95
|
+
const effectiveTtlSeconds = ttlSeconds ?? this.defaultTtlSeconds;
|
|
96
|
+
if (entry) this.currentBytes -= entry.size;
|
|
97
|
+
this.cache.delete(key);
|
|
98
|
+
const size = 8;
|
|
99
|
+
this.cache.set(key, {
|
|
100
|
+
value: next,
|
|
101
|
+
size,
|
|
102
|
+
expiresAt: live ? entry.expiresAt : now + Math.max(1, effectiveTtlSeconds) * 1e3
|
|
103
|
+
});
|
|
104
|
+
this.currentBytes += size;
|
|
105
|
+
this.evictToLimit();
|
|
106
|
+
return next;
|
|
107
|
+
}
|
|
79
108
|
async clear(pattern) {
|
|
80
109
|
if (pattern === void 0) {
|
|
81
110
|
this.cache.clear();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-
|
|
1
|
+
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DPRH0Cme.mjs";
|
|
2
2
|
import { RouteHandlerMethod } from "fastify";
|
|
3
3
|
//#region src/middleware/middleware.d.ts
|
|
4
4
|
interface NamedMiddleware {
|
|
@@ -90,6 +90,14 @@ interface MultipartBodyOptions {
|
|
|
90
90
|
* @example ['avatar', 'cover'] // multi-field upload (profile editor)
|
|
91
91
|
*/
|
|
92
92
|
requiredFields?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* Cap on the TOTAL buffered file bytes per request (default: 25 MiB).
|
|
95
|
+
* Per-file and per-count limits bound individual files but not their
|
|
96
|
+
* SUM — without this, `maxFiles: 5` × `maxFileSize: 10 MiB` lets one
|
|
97
|
+
* request retain ~50 MiB of process memory, and concurrency multiplies
|
|
98
|
+
* it (20 such uploads ≈ 1 GiB). Exceeding the cap returns 413.
|
|
99
|
+
*/
|
|
100
|
+
maxTotalBytes?: number;
|
|
93
101
|
}
|
|
94
102
|
/**
|
|
95
103
|
* Create a multipart body parsing middleware.
|
|
@@ -102,6 +110,14 @@ interface MultipartBodyOptions {
|
|
|
102
110
|
* For non-multipart requests (regular JSON), this is a no-op — the request
|
|
103
111
|
* passes through unchanged. This makes it safe to add to create/update
|
|
104
112
|
* middlewares without breaking JSON clients.
|
|
113
|
+
*
|
|
114
|
+
* **BUFFERED — for small uploads only.** Every accepted file is held fully
|
|
115
|
+
* in process memory for the request's lifetime; memory cost is
|
|
116
|
+
* `concurrency × maxTotalBytes`. Suitable for avatars, documents, and
|
|
117
|
+
* form attachments. For large files use PRESIGNED direct-to-object-storage
|
|
118
|
+
* uploads (see the files-upload preset / production guide) and pair upload
|
|
119
|
+
* routes with strict rate limits — buffering video through the app tier is
|
|
120
|
+
* the wrong architecture regardless of limits.
|
|
105
121
|
*/
|
|
106
122
|
declare function multipartBody(options?: MultipartBodyOptions): RouteHandlerMethod;
|
|
107
123
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
|
|
2
|
-
import { t as multipartBody } from "../multipartBody-
|
|
2
|
+
import { t as multipartBody } from "../multipartBody-D0eBvkeq.mjs";
|
|
3
3
|
//#region src/middleware/middleware.ts
|
|
4
4
|
/**
|
|
5
5
|
* Named Middleware — Priority-based, conditional middleware execution.
|