@classytic/arc 2.22.0 → 2.23.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.
Files changed (86) hide show
  1. package/dist/{BaseController-C6FlRWxF.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +1 -1
  6. package/dist/auth/index.d.mts +1 -1
  7. package/dist/auth/index.mjs +2 -2
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-b6fgFgnd.mjs → core-DQ6rDTyH.mjs} +10 -9
  17. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-BuOv-m4Y.mjs} +3 -3
  18. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-CgJv_BU_.mjs} +1 -1
  19. package/dist/{createApp-Ci7yG5gv.mjs → createApp-CjOeWJ9_.mjs} +11 -2
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  23. package/dist/events/index.mjs +5 -2
  24. package/dist/factory/index.d.mts +20 -4
  25. package/dist/factory/index.mjs +31 -11
  26. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-g_f9omSc.d.mts} +1 -1
  27. package/dist/hooks/index.d.mts +1 -1
  28. package/dist/idempotency/index.d.mts +7 -0
  29. package/dist/idempotency/index.mjs +5 -17
  30. package/dist/{index-DbtgCOCj.d.mts → index-BBsvQt7U.d.mts} +26 -4
  31. package/dist/{index-BEu4wjbL.d.mts → index-Ba10fdKp.d.mts} +11 -2
  32. package/dist/{index-D8MKYGDa.d.mts → index-CC5ruDGh.d.mts} +2 -2
  33. package/dist/{index-ogIHvOjo.d.mts → index-wMoWBLkn.d.mts} +67 -1
  34. package/dist/index.d.mts +4 -4
  35. package/dist/index.mjs +6 -5
  36. package/dist/{init-DleK3p9f.mjs → init-CjO4p_q1.mjs} +5 -2
  37. package/dist/integrations/event-gateway.mjs +1 -1
  38. package/dist/integrations/index.d.mts +1 -1
  39. package/dist/integrations/mcp/index.d.mts +66 -3
  40. package/dist/integrations/mcp/index.mjs +1 -1
  41. package/dist/integrations/mcp/testing.d.mts +1 -1
  42. package/dist/integrations/mcp/testing.mjs +1 -1
  43. package/dist/integrations/webhooks.d.mts +123 -3
  44. package/dist/integrations/webhooks.mjs +160 -5
  45. package/dist/interface-n5hpKg1d.mjs +16 -0
  46. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  47. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-BtzIL_kM.mjs} +308 -39
  48. package/dist/middleware/index.d.mts +1 -1
  49. package/dist/{openapi-BRpMZD7C.mjs → openapi-jSDx-VDv.mjs} +3 -3
  50. package/dist/permissions/index.d.mts +2 -2
  51. package/dist/permissions/index.mjs +3 -2
  52. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  53. package/dist/pipeline/index.d.mts +1 -1
  54. package/dist/plugins/index.d.mts +6 -6
  55. package/dist/plugins/index.mjs +5 -4
  56. package/dist/plugins/tracing-entry.mjs +1 -1
  57. package/dist/presets/filesUpload.d.mts +1 -1
  58. package/dist/presets/filesUpload.mjs +1 -1
  59. package/dist/presets/index.d.mts +1 -1
  60. package/dist/presets/index.mjs +1 -1
  61. package/dist/presets/multiTenant.d.mts +1 -1
  62. package/dist/presets/search.d.mts +1 -1
  63. package/dist/presets/search.mjs +1 -1
  64. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  65. package/dist/realtime-I8OrfnMR.mjs +375 -0
  66. package/dist/{registerRoute-xHc42fwB.mjs → registerRoute-CWO6mdYe.mjs} +1 -1
  67. package/dist/registry/index.d.mts +2 -2
  68. package/dist/registry/index.mjs +1 -1
  69. package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  70. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  71. package/dist/scope/index.mjs +1 -1
  72. package/dist/scope-LONQ3mfZ.mjs +37 -0
  73. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  74. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  75. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  76. package/dist/testing/index.d.mts +2 -2
  77. package/dist/testing/index.mjs +2 -2
  78. package/dist/types/index.d.mts +1 -1
  79. package/dist/{types-Bc42CUyk.d.mts → types-5OuqeTDX.d.mts} +37 -2
  80. package/dist/utils/index.d.mts +62 -2
  81. package/dist/utils/index.mjs +5 -3
  82. package/dist/{utils-FhJ88TJE.mjs → utils-CZyeRXTh.mjs} +21 -69
  83. package/dist/{versioning-DLwklPlG.d.mts → versioning-D6PvCwpq.d.mts} +43 -4
  84. package/package.json +64 -4
  85. package/skills/arc/references/production.md +734 -696
  86. package/dist/scope-k15RXfcq.mjs +0 -64
@@ -1,13 +1,14 @@
1
1
  import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
2
- import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
3
- import { t as BaseController } from "./BaseController-C6FlRWxF.mjs";
4
- import { P as normalizePermissionResult } from "./permissions-BJMuKMl7.mjs";
2
+ import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-bHblZ9YN.mjs";
3
+ import { t as BaseController } from "./BaseController-BQ8KNvWC.mjs";
4
+ import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
5
5
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
6
- import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-ze70xXJi.mjs";
6
+ import { d as buildRequestScopeProjection, i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
7
7
  import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
8
8
  import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
9
9
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
10
- import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-B-6SPe7g.mjs";
10
+ import { t as createIdempotencyResult } from "./interface-n5hpKg1d.mjs";
11
+ import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
11
12
  import { createHash, randomUUID } from "node:crypto";
12
13
  import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
13
14
  import fp from "fastify-plugin";
@@ -160,6 +161,13 @@ function registerPrompt(server, prompt) {
160
161
  //#endregion
161
162
  //#region src/integrations/mcp/buildRequestContext.ts
162
163
  /**
164
+ * @classytic/arc — MCP → IRequestContext Bridge
165
+ *
166
+ * Builds an IRequestContext from MCP tool input + session identity.
167
+ * Ensures MCP tool handlers go through the same BaseController pipeline
168
+ * as REST requests (AccessControl, BodySanitizer, QueryResolver, HookSystem).
169
+ */
170
+ /**
163
171
  * Build an IRequestContext from MCP tool input and session auth.
164
172
  *
165
173
  * | Operation | params | query | body |
@@ -177,7 +185,7 @@ function registerPrompt(server, prompt) {
177
185
  * the session-derived scope is `public` (i.e. MCP called with `auth: false`).
178
186
  * An authenticated session scope is never overwritten.
179
187
  */
180
- function buildRequestContext(input, auth, operation, policyFilters, scopeOverride) {
188
+ function buildRequestContext(input, auth, operation, policyFilters, scopeOverride, extras) {
181
189
  const sessionScope = buildScope(auth);
182
190
  const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
183
191
  const base = {
@@ -188,10 +196,13 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
188
196
  } : null,
189
197
  headers: {},
190
198
  context: {},
199
+ scope: buildRequestScopeProjection(scope),
191
200
  metadata: {
192
201
  _scope: scope,
193
- _policyFilters: policyFilters ?? {}
194
- }
202
+ _policyFilters: policyFilters ?? {},
203
+ ...extras?.arc ? { arc: extras.arc } : {}
204
+ },
205
+ ...extras?.server ? { server: extras.server } : {}
195
206
  };
196
207
  switch (operation) {
197
208
  case "list": return {
@@ -375,6 +386,42 @@ async function evaluatePermission(check, session, resource, action, input) {
375
386
  }));
376
387
  }
377
388
  /**
389
+ * Apply field-level READ permissions to an MCP tool payload — the same
390
+ * masking `sendControllerResponse` applies on the HTTP wire and the
391
+ * realtime plugin applies per frame. Without this, a resource declaring
392
+ * `fields: { salary: fields.visibleTo(['admin']) }` masked salary over
393
+ * REST and SSE but leaked it verbatim through every MCP tool.
394
+ *
395
+ * Mirrors the HTTP adapter's semantics exactly:
396
+ * - Elevated scope (platform admin) bypasses masking — consistent with
397
+ * `requireOrgRole()` and `BodySanitizer` bypass logic.
398
+ * - Effective roles = session roles ∪ org roles (member scope only),
399
+ * via the shared `resolveEffectiveRoles`.
400
+ * - `scopeOverride` (a `PermissionResult.scope`) follows the same
401
+ * non-downgrade rule as `buildRequestContext`: honored only when the
402
+ * session-derived scope is `public`.
403
+ *
404
+ * Handles the three payload shapes MCP tools emit: a single record, an
405
+ * array of records, and a paginated result object carrying an inner
406
+ * `data: T[]`. Scalars and `null` pass through untouched.
407
+ */
408
+ function applyMcpReadMasking(data, options) {
409
+ const { fields, session, scopeOverride } = options;
410
+ if (!fields || data === null || typeof data !== "object") return data;
411
+ const sessionScope = buildScope(session);
412
+ const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
413
+ if (isElevated(scope)) return data;
414
+ const roles = resolveEffectiveRoles(session?.roles ?? [], isMember(scope) ? scope.orgRoles : []);
415
+ const maskItem = (item) => item !== null && typeof item === "object" && !Array.isArray(item) ? applyFieldReadPermissions(item, fields, roles) : item;
416
+ if (Array.isArray(data)) return data.map(maskItem);
417
+ const inner = data.data;
418
+ if (Array.isArray(inner)) return {
419
+ ...data,
420
+ data: inner.map(maskItem)
421
+ };
422
+ return maskItem(data);
423
+ }
424
+ /**
378
425
  * Convert a controller response envelope into an MCP `CallToolResult`.
379
426
  * Carries `meta` into the serialized payload so consumers see pagination
380
427
  * totals, stripped-field arrays, etc.
@@ -476,6 +523,78 @@ function createMcpController(resource) {
476
523
  //#endregion
477
524
  //#region src/integrations/mcp/invokeController.ts
478
525
  /**
526
+ * @classytic/arc — Non-HTTP controller invocation for MCP / jobs / workflows.
527
+ *
528
+ * Internal callers (MCP tools, scheduled jobs, workflow steps) need to invoke
529
+ * controller methods without going through Fastify's preHandler chain.
530
+ * Before this module landed, every call site reinvented the same shim:
531
+ * fabricate `params` / `body` / `headers`, project the session into a
532
+ * `RequestScope`, run the permission check by hand, translate thrown errors
533
+ * to the MCP `CallToolResult` shape. The drift across those copies is what
534
+ * the mentora review flagged as the "buildCtx synthetic-context hack."
535
+ *
536
+ * Two public surfaces, layered so callers reach for the smallest piece that
537
+ * does the job:
538
+ *
539
+ * - {@link invokeController} full pipeline: permission → context →
540
+ * dispatch → canonical envelope/error
541
+ * conversion. Returns `CallToolResult`.
542
+ * - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
543
+ * async function into a `ToolDefinition`
544
+ * handler with canonical error shape. Use
545
+ * when you want to compose multiple
546
+ * controller calls or weave in custom logic
547
+ * between them.
548
+ *
549
+ * Both honor the same {@link buildRequestContext} primitive — re-exported
550
+ * from this module so authors who want the synthetic context as data (e.g.
551
+ * to pass into `executePipeline`) can grab it without depending on an
552
+ * internal subpath.
553
+ */
554
+ /**
555
+ * Assemble the {@link McpContextExtras} for a controller invocation from
556
+ * resource-scoped metadata + app-level wiring. Mirrors what
557
+ * `buildArcDecorator` (metadata.arc) and `createRequestContext` (server
558
+ * accessor) assemble on the HTTP path.
559
+ */
560
+ function buildContextExtras(options) {
561
+ const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
562
+ if (!wiring) return void 0;
563
+ return {
564
+ arc: {
565
+ resourceName,
566
+ schemaOptions,
567
+ permissions,
568
+ hooks: wiring.hooks,
569
+ events: wiring.events,
570
+ fields,
571
+ idField
572
+ },
573
+ server: {
574
+ events: wiring.events,
575
+ audit: wiring.audit,
576
+ log: wiring.log
577
+ }
578
+ };
579
+ }
580
+ /**
581
+ * Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
582
+ * Scopes an idempotency key to (resource, action, input, caller) so a
583
+ * reused key with DIFFERENT input is a fresh execution, matching the HTTP
584
+ * plugin's method+url+bodyHash+user fingerprint semantics.
585
+ */
586
+ function invocationFingerprint(resourceName, actionName, input, session) {
587
+ const sortKeys = (value) => {
588
+ if (Array.isArray(value)) return value.map(sortKeys);
589
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
590
+ return value;
591
+ };
592
+ return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
593
+ }
594
+ /** TTLs matching the HTTP idempotency plugin's defaults. */
595
+ const MCP_IDEMPOTENCY_TTL_MS = 1440 * 60 * 1e3;
596
+ const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
597
+ /**
479
598
  * Invoke a controller method through the MCP synthetic-context path.
480
599
  *
481
600
  * Runs the same chain CRUD MCP tools run:
@@ -508,7 +627,7 @@ function createMcpController(resource) {
508
627
  * checks compose.
509
628
  */
510
629
  async function invokeController(controller, op, input, options) {
511
- const { session, resourceName, actionName = op, permissions, methodName = op } = options;
630
+ const { session, resourceName, actionName = op, permissions, methodName = op, fields, wiring, idempotencyKey } = options;
512
631
  const ctrl = controller;
513
632
  try {
514
633
  const method = ctrl[methodName];
@@ -524,7 +643,39 @@ async function invokeController(controller, op, input, options) {
524
643
  reason: permResult.reason,
525
644
  session
526
645
  });
527
- return toCallToolResult(await method(buildRequestContext(input, session, op, permResult?.filters, permResult?.scope)));
646
+ const dispatch = async () => {
647
+ const reqCtx = buildRequestContext(input, session, op, permResult?.filters, permResult?.scope, buildContextExtras(options));
648
+ const response = await method(reqCtx);
649
+ const data = applyMcpReadMasking(response.data, {
650
+ fields,
651
+ session,
652
+ scopeOverride: permResult?.scope
653
+ });
654
+ return toCallToolResult(data === response.data ? response : {
655
+ ...response,
656
+ data
657
+ });
658
+ };
659
+ const store = wiring?.idempotencyStore;
660
+ if (idempotencyKey && store) {
661
+ const fullKey = `${idempotencyKey}:${invocationFingerprint(resourceName, actionName, input, session)}`;
662
+ const cached = await store.get(fullKey);
663
+ if (cached) return cached.body;
664
+ const lockId = randomUUID();
665
+ if (!await store.tryLock(fullKey, lockId, MCP_IDEMPOTENCY_LOCK_MS)) return toCallToolError({
666
+ code: "arc.idempotency_conflict",
667
+ message: "A call with this idempotency key is already in progress",
668
+ status: 409
669
+ });
670
+ try {
671
+ const result = await dispatch();
672
+ if (!result.isError) await store.set(fullKey, createIdempotencyResult(200, result, {}, MCP_IDEMPOTENCY_TTL_MS));
673
+ return result;
674
+ } finally {
675
+ await store.unlock(fullKey, lockId);
676
+ }
677
+ }
678
+ return await dispatch();
528
679
  } catch (err) {
529
680
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
530
681
  }
@@ -605,13 +756,33 @@ const CRUD_ANNOTATIONS = {
605
756
  * build + envelope/error conversion all live in one place — this factory
606
757
  * only resolves the op-specific permission and threads it through.
607
758
  */
608
- function createCrudHandler(op, controller, resourceName, permissions) {
759
+ /** Ops where an idempotency key is meaningful (mutations). */
760
+ const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
761
+ "create",
762
+ "update",
763
+ "delete"
764
+ ]);
765
+ function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
609
766
  const permission = permissions?.[op];
610
- return (input, ctx) => invokeController(controller, op, input, {
611
- session: ctx.session,
612
- resourceName,
613
- permissions: permission
614
- });
767
+ return (input, ctx) => {
768
+ let idempotencyKey;
769
+ let payload = input;
770
+ if ("_idempotencyKey" in input) {
771
+ const { _idempotencyKey, ...rest } = input;
772
+ payload = rest;
773
+ if (MUTATING_CRUD_OPS.has(op) && typeof _idempotencyKey === "string" && _idempotencyKey) idempotencyKey = _idempotencyKey;
774
+ }
775
+ return invokeController(controller, op, payload, {
776
+ session: ctx.session,
777
+ resourceName,
778
+ permissions: permission,
779
+ fields,
780
+ wiring: extras?.wiring,
781
+ schemaOptions: extras?.schemaOptions,
782
+ idField: extras?.idField,
783
+ idempotencyKey
784
+ });
785
+ };
615
786
  }
616
787
  /**
617
788
  * Default description for a CRUD tool. Enriches list descriptions with the
@@ -1008,7 +1179,24 @@ function convertActionSchemaToZod(raw) {
1008
1179
  * in the 2.10.8 review: REST and MCP action tools share identical
1009
1180
  * context-building machinery.
1010
1181
  */
1011
- function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
1182
+ function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
1183
+ const wiring = extras?.wiring;
1184
+ const contextExtras = wiring ? {
1185
+ arc: {
1186
+ resourceName,
1187
+ schemaOptions: extras?.schemaOptions,
1188
+ permissions,
1189
+ hooks: wiring.hooks,
1190
+ events: wiring.events,
1191
+ fields,
1192
+ idField: extras?.idField
1193
+ },
1194
+ server: {
1195
+ events: wiring.events,
1196
+ audit: wiring.audit,
1197
+ log: wiring.log
1198
+ }
1199
+ } : void 0;
1012
1200
  const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
1013
1201
  const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
1014
1202
  return async (input, ctx) => {
@@ -1036,11 +1224,15 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1036
1224
  const reqCtx = buildRequestContext({
1037
1225
  ...input,
1038
1226
  action: actionName
1039
- }, session, "action", permResult?.filters, permResult?.scope);
1227
+ }, session, "action", permResult?.filters, permResult?.scope, contextExtras);
1040
1228
  const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
1041
1229
  const { id: _discardId, ...data } = input;
1042
1230
  try {
1043
- return toCallToolSuccess(await handler(id, data, reqCtx));
1231
+ return toCallToolSuccess(applyMcpReadMasking(await handler(id, data, reqCtx), {
1232
+ fields,
1233
+ session,
1234
+ scopeOverride: permResult?.scope
1235
+ }));
1044
1236
  } catch (err) {
1045
1237
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1046
1238
  }
@@ -1055,7 +1247,7 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1055
1247
  * every aggregation opts out via `mcp: false`.
1056
1248
  */
1057
1249
  function buildAggregationTools(args) {
1058
- const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix } = args;
1250
+ const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix, fields } = args;
1059
1251
  if (!aggregations || Object.keys(aggregations).length === 0) return [];
1060
1252
  const normalized = validateAggregations(resourceName, aggregations, schemaOptions);
1061
1253
  const tools = [];
@@ -1079,7 +1271,8 @@ function buildAggregationTools(args) {
1079
1271
  permissions: config.permissions,
1080
1272
  resourceName,
1081
1273
  repo,
1082
- buildOptionsFromSession
1274
+ buildOptionsFromSession,
1275
+ fields
1083
1276
  })
1084
1277
  });
1085
1278
  }
@@ -1121,7 +1314,7 @@ function buildAggregationInputSchema(norm) {
1121
1314
  return shape;
1122
1315
  }
1123
1316
  function createAggregationToolHandler(args) {
1124
- const { normalized, permissions, resourceName, repo, buildOptionsFromSession } = args;
1317
+ const { normalized, permissions, resourceName, repo, buildOptionsFromSession, fields } = args;
1125
1318
  return async (input, ctx) => {
1126
1319
  const permResult = await evaluatePermission(permissions, ctx.session, resourceName, `aggregation:${normalized.name}`, input);
1127
1320
  if (permResult && !permResult.granted) return permissionDeniedResult({
@@ -1140,7 +1333,21 @@ function createAggregationToolHandler(args) {
1140
1333
  query: filterInput,
1141
1334
  tenantOptions
1142
1335
  });
1143
- if (result.status === 200) return toCallToolSuccess(result.body);
1336
+ if (result.status === 200) {
1337
+ const body = result.body;
1338
+ if (fields && Array.isArray(body?.rows)) {
1339
+ const rows = applyMcpReadMasking(body.rows, {
1340
+ fields,
1341
+ session: ctx.session,
1342
+ scopeOverride: permResult?.scope
1343
+ });
1344
+ return toCallToolSuccess({
1345
+ ...body,
1346
+ rows
1347
+ });
1348
+ }
1349
+ return toCallToolSuccess(result.body);
1350
+ }
1144
1351
  return toCallToolError(result.body);
1145
1352
  };
1146
1353
  }
@@ -1480,8 +1687,24 @@ function operationKindForRoute(method, hasId) {
1480
1687
  function createCustomRouteHandler(route, controller, hasId, options) {
1481
1688
  const ctrl = controller;
1482
1689
  const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
1483
- const { resourceName, operationName, permissions, pipeline } = options;
1690
+ const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
1484
1691
  const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
1692
+ const contextExtras = wiring ? {
1693
+ arc: {
1694
+ resourceName,
1695
+ schemaOptions: options.schemaOptions,
1696
+ permissions,
1697
+ hooks: wiring.hooks,
1698
+ events: wiring.events,
1699
+ fields,
1700
+ idField: options.idField
1701
+ },
1702
+ server: {
1703
+ events: wiring.events,
1704
+ audit: wiring.audit,
1705
+ log: wiring.log
1706
+ }
1707
+ } : void 0;
1485
1708
  return async (input, _ctx) => {
1486
1709
  const session = _ctx.session;
1487
1710
  const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
@@ -1492,7 +1715,18 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1492
1715
  session
1493
1716
  });
1494
1717
  try {
1495
- const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
1718
+ const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope, contextExtras);
1719
+ const emit = (envelope) => {
1720
+ const data = applyMcpReadMasking(envelope.data, {
1721
+ fields,
1722
+ session,
1723
+ scopeOverride: permResult?.scope
1724
+ });
1725
+ return toCallToolResult(data === envelope.data ? envelope : {
1726
+ ...envelope,
1727
+ data
1728
+ });
1729
+ };
1496
1730
  if (typeof route.handler === "function") {
1497
1731
  const fn = route.handler;
1498
1732
  if (pipelineSteps.length > 0) {
@@ -1501,13 +1735,13 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1501
1735
  resource: resourceName,
1502
1736
  operation: operationName
1503
1737
  };
1504
- return toCallToolResult(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1738
+ return emit(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1505
1739
  const raw = await fn(ctx);
1506
1740
  return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1507
1741
  }, operationName));
1508
1742
  }
1509
1743
  const out = await fn(reqCtx);
1510
- return toCallToolResult(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1744
+ return emit(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1511
1745
  }
1512
1746
  if (!ctrl) return {
1513
1747
  content: [{
@@ -1524,7 +1758,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1524
1758
  }],
1525
1759
  isError: true
1526
1760
  };
1527
- return toCallToolResult(await method(reqCtx));
1761
+ return emit(await method(reqCtx));
1528
1762
  } catch (err) {
1529
1763
  return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1530
1764
  }
@@ -1598,6 +1832,7 @@ function resourceToTools(resource, config = {}) {
1598
1832
  const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
1599
1833
  const tools = [];
1600
1834
  const prefix = config.toolNamePrefix;
1835
+ const wiring = config.wiring;
1601
1836
  if (controller) {
1602
1837
  let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
1603
1838
  if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
@@ -1608,6 +1843,15 @@ function resourceToTools(resource, config = {}) {
1608
1843
  allowedOperators,
1609
1844
  sortableFields
1610
1845
  });
1846
+ const inputSchema = buildInputSchema(op, fieldRules, {
1847
+ hiddenFields,
1848
+ readonlyFields,
1849
+ extraHideFields: config.hideFields,
1850
+ filterableFields,
1851
+ allowedOperators,
1852
+ adapterBodies
1853
+ });
1854
+ 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
1855
  tools.push({
1612
1856
  name,
1613
1857
  description: resolveCrudDescription(config.descriptions?.[op], {
@@ -1620,15 +1864,12 @@ function resourceToTools(resource, config = {}) {
1620
1864
  sortableFields
1621
1865
  }),
1622
1866
  annotations: CRUD_ANNOTATIONS[op],
1623
- inputSchema: buildInputSchema(op, fieldRules, {
1624
- hiddenFields,
1625
- readonlyFields,
1626
- extraHideFields: config.hideFields,
1627
- filterableFields,
1628
- allowedOperators,
1629
- adapterBodies
1867
+ inputSchema,
1868
+ handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
1869
+ wiring,
1870
+ schemaOptions: resource.schemaOptions,
1871
+ idField: resource.idField
1630
1872
  }),
1631
- handler: createCrudHandler(op, controller, resource.name, resource.permissions),
1632
1873
  source: `crud:${resource.name}:${op}`
1633
1874
  });
1634
1875
  }
@@ -1680,7 +1921,11 @@ function resourceToTools(resource, config = {}) {
1680
1921
  resourceName: resource.name,
1681
1922
  operationName: opName,
1682
1923
  permissions: route.permissions,
1683
- pipeline: resource.pipe
1924
+ pipeline: resource.pipe,
1925
+ fields: resource.fields,
1926
+ wiring,
1927
+ schemaOptions: resource.schemaOptions,
1928
+ idField: resource.idField
1684
1929
  })
1685
1930
  });
1686
1931
  }
@@ -1711,7 +1956,11 @@ function resourceToTools(resource, config = {}) {
1711
1956
  annotations,
1712
1957
  inputSchema: inputShape,
1713
1958
  source: `action:${resource.name}:${actionName}`,
1714
- handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess)
1959
+ handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
1960
+ wiring,
1961
+ schemaOptions: resource.schemaOptions,
1962
+ idField: resource.idField
1963
+ })
1715
1964
  });
1716
1965
  }
1717
1966
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
@@ -1738,7 +1987,8 @@ function resourceToTools(resource, config = {}) {
1738
1987
  schemaOptions: resource.schemaOptions,
1739
1988
  repo: repoForAgg,
1740
1989
  buildOptionsFromSession,
1741
- prefix
1990
+ prefix,
1991
+ fields: resource.fields
1742
1992
  }));
1743
1993
  }
1744
1994
  return tools;
@@ -1948,10 +2198,29 @@ const mcpPluginImpl = async (fastify, options) => {
1948
2198
  include: options.include,
1949
2199
  exclude: options.exclude
1950
2200
  });
2201
+ const fastifyAny = fastify;
2202
+ const wiring = {
2203
+ get hooks() {
2204
+ return fastifyAny.arc?.hooks;
2205
+ },
2206
+ get events() {
2207
+ return fastifyAny.events;
2208
+ },
2209
+ get audit() {
2210
+ return fastifyAny.audit;
2211
+ },
2212
+ get log() {
2213
+ return fastify.log;
2214
+ },
2215
+ get idempotencyStore() {
2216
+ return fastifyAny.idempotency?.store;
2217
+ }
2218
+ };
1951
2219
  const overrides = options.overrides ?? {};
1952
2220
  const allTools = enabledResources.flatMap((r) => {
1953
2221
  const resOverrides = overrides[r.name] ?? {};
1954
2222
  return resourceToTools(r, {
2223
+ wiring,
1955
2224
  ...resOverrides,
1956
2225
  toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
1957
2226
  });
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-DbtgCOCj.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-BBsvQt7U.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
  //#region src/middleware/middleware.d.ts
4
4
  interface NamedMiddleware {
@@ -1,8 +1,8 @@
1
1
  import { t as getUserRoles } from "./types-XTApa8b6.mjs";
2
- import { n as convertRouteSchema } from "./schemaConverter-Ds5reSGs.mjs";
3
- import { t as resolveActionPermission } from "./actionPermissions-ze70xXJi.mjs";
2
+ import { n as convertRouteSchema } from "./schemaConverter-DJvodiHj.mjs";
3
+ import { t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
4
4
  import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
5
- import { t as buildActionBodySchema } from "./createActionRouter-h1cEEaEX.mjs";
5
+ import { t as buildActionBodySchema } from "./createActionRouter-BuOv-m4Y.mjs";
6
6
  import fp from "fastify-plugin";
7
7
  //#region src/docs/openapi/canonical-schemas.ts
8
8
  /**
@@ -1,4 +1,4 @@
1
1
  import { a as getUserRoles, i as UserBase, n as PermissionContext, o as normalizeRoles, r as PermissionResult, t as PermissionCheck } from "../types-Csxga7d3.mjs";
2
2
  import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission } from "../fields-D3lhgzhD.mjs";
3
- import { A as requireOwnership, B as requireMandate, C as createOrgPermissions, D as denyAll, E as anyOf, F as normalizePermissionResult, I as RequireAgentScopeOptions, L as RequireMandateOptions, M as roles, N as when, O as not, P as applyPermissionResult, R as requireAgentScope, S as createDynamicPermissionMatrix, T as allowPublic, _ as createRoleHierarchy, a as permissionMatrix, b as DynamicPermissionMatrixConfig, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as requireRoles, k as requireAuth, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as ConnectEventsOptions, w as allOf, x as PermissionEventBus, y as DynamicPermissionMatrix, z as requireDPoP } from "../index-ogIHvOjo.mjs";
4
- export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
3
+ import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-wMoWBLkn.mjs";
4
+ export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, GRANT_MODES, type GrantMode, type GrantResolution, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireGrantOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
@@ -1,3 +1,4 @@
1
- import { A as requireDPoP, C as not, D as roles, E as requireRoles, F as applyFieldReadPermissions, I as applyFieldWritePermissions, L as fields, M as applyPermissionResult, O as when, P as normalizePermissionResult, R as resolveEffectiveRoles, S as denyAll, T as requireOwnership, _ as requireServiceScope, a as permissionMatrix, b as allowPublic, c as publicReadAdminWrite, d as createDynamicPermissionMatrix, f as createOrgPermissions, g as requireScopeContext, h as requireOrgRole, i as ownerWithAdminBypass, j as requireMandate, k as requireAgentScope, l as readOnly, m as requireOrgMembership, n as authenticated, o as presets_exports, p as requireOrgInScope, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireTeamMembership, w as requireAuth, x as anyOf, y as allOf } from "../permissions-BJMuKMl7.mjs";
1
+ import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, o as fields, r as normalizePermissionResult, s as resolveEffectiveRoles, t as applyPermissionResult } from "../applyPermissionResult-Tw4iXtlf.mjs";
2
2
  import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
3
- export { adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
3
+ import { A as roles, C as allowPublic, D as requireAuth, E as not, M as requireAgentScope, N as requireDPoP, O as requireOwnership, P as requireMandate, S as allOf, T as denyAll, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as when, k as requireRoles, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as anyOf, x as requireTeamMembership, y as requireScopeContext } from "../permissions-tbxFHxjZ.mjs";
4
+ export { GRANT_MODES, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };