@classytic/arc 2.19.0 → 2.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +1 -1
  2. package/bin/arc.js +37 -0
  3. package/dist/{BaseController-Dy6EQHjM.mjs → BaseController-D-MHZ27_.mjs} +4 -4
  4. package/dist/{ResourceRegistry-5fKZwhK_.mjs → ResourceRegistry-7RnrEWNP.mjs} +1 -1
  5. package/dist/{actionPermissions-D7NGwcAR.mjs → actionPermissions-C9RMiIpc.mjs} +1 -1
  6. package/dist/audit/index.mjs +1 -1
  7. package/dist/auth/index.d.mts +1 -1
  8. package/dist/auth/index.mjs +7 -5
  9. package/dist/{buildHandler-B0qvAguf.mjs → buildHandler-D_2ULWxZ.mjs} +3 -3
  10. package/dist/cache/index.mjs +2 -2
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/describe.mjs +2 -2
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/generate.mjs +1 -1
  15. package/dist/cli/commands/init.mjs +1 -1
  16. package/dist/cli/commands/introspect.mjs +4 -13
  17. package/dist/cli/index.mjs +2 -2
  18. package/dist/{constants-gaEeMlz-.mjs → constants-DNre0wRg.mjs} +1 -1
  19. package/dist/core/index.d.mts +2 -2
  20. package/dist/core/index.mjs +4 -4
  21. package/dist/{core-Ba-6Sue6.mjs → core-CWhyGkiM.mjs} +125 -53
  22. package/dist/{createActionRouter-B_WzBFyf.mjs → createActionRouter-HlQso0jO.mjs} +4 -4
  23. package/dist/{createAggregationRouter-B_G_nIjm.mjs → createAggregationRouter-5LzPzgw1.mjs} +5 -4
  24. package/dist/{createApp--703hQE1.mjs → createApp-D6uwzCKu.mjs} +210 -17
  25. package/dist/docs/index.d.mts +1 -1
  26. package/dist/docs/index.mjs +1 -1
  27. package/dist/events/index.mjs +9 -6
  28. package/dist/factory/index.d.mts +3 -2
  29. package/dist/factory/index.mjs +2 -2
  30. package/dist/{fastify-augmentation-D-5_yQJi.d.mts → fastify-augmentation-CCQJEDgh.d.mts} +12 -1
  31. package/dist/{generate-Brans3fW.mjs → generate-usRSdQmP.mjs} +4 -3
  32. package/dist/hooks/index.d.mts +1 -1
  33. package/dist/idempotency/index.mjs +1 -1
  34. package/dist/{index-CRKiiP5o.d.mts → index-CdFkIMEd.d.mts} +5 -0
  35. package/dist/{index-CP6A1Ffj.d.mts → index-DUa8WHz3.d.mts} +1 -1
  36. package/dist/{index-CUTG2Cuo.d.mts → index-_--GyzcO.d.mts} +28 -1
  37. package/dist/index.d.mts +3 -3
  38. package/dist/index.mjs +6 -6
  39. package/dist/{init-Bqpt1Hxf.mjs → init-YH_nslQR.mjs} +87 -5
  40. package/dist/integrations/index.d.mts +1 -1
  41. package/dist/integrations/mcp/index.d.mts +2 -2
  42. package/dist/integrations/mcp/index.mjs +1 -1
  43. package/dist/integrations/mcp/testing.d.mts +1 -1
  44. package/dist/integrations/mcp/testing.mjs +1 -1
  45. package/dist/integrations/streamline.d.mts +17 -5
  46. package/dist/integrations/streamline.mjs +9 -5
  47. package/dist/{loadResourcesFromEntry-BTlFEx05.mjs → loadResourcesFromEntry-OMCvIEE7.mjs} +1 -1
  48. package/dist/{mcpPlugin-C088Nd-G.mjs → mcpPlugin-CuW6L_uD.mjs} +148 -49
  49. package/dist/middleware/index.d.mts +1 -1
  50. package/dist/middleware/index.mjs +1 -1
  51. package/dist/module-JYH9V0I8.d.mts +384 -0
  52. package/dist/{openapi-DZ-5a5Op.mjs → openapi-Dq6RbEGK.mjs} +3 -3
  53. package/dist/permissions/index.d.mts +1 -1
  54. package/dist/permissions/index.mjs +1 -1
  55. package/dist/{permissions-BPB1xqGf.mjs → permissions-CgbOLJZG.mjs} +10 -2
  56. package/dist/pipeline/index.d.mts +1 -1
  57. package/dist/plugins/index.d.mts +10 -5
  58. package/dist/plugins/index.mjs +3 -3
  59. package/dist/plugins/tracing-entry.mjs +1 -1
  60. package/dist/{pluralize-DccFV9R7.mjs → pluralize-BhrgwZb9.mjs} +3 -3
  61. package/dist/presets/filesUpload.d.mts +1 -1
  62. package/dist/presets/filesUpload.mjs +1 -1
  63. package/dist/presets/index.d.mts +16 -2
  64. package/dist/presets/index.mjs +1 -1
  65. package/dist/presets/multiTenant.d.mts +1 -1
  66. package/dist/presets/multiTenant.mjs +1 -1
  67. package/dist/presets/search.d.mts +1 -1
  68. package/dist/presets/search.mjs +3 -2
  69. package/dist/{presets-BtyFwTO4.mjs → presets-DWsYQdbV.mjs} +1 -1
  70. package/dist/{queryCachePlugin-emHb0CuX.mjs → queryCachePlugin-DzqLN904.mjs} +2 -2
  71. package/dist/{registerRoute-BdV8DDwU.mjs → registerRoute-DGSREwPw.mjs} +1 -1
  72. package/dist/registry/index.d.mts +1 -1
  73. package/dist/registry/index.mjs +2 -2
  74. package/dist/{registry-Bm9LjWKs.mjs → registry-vcOCtPij.mjs} +1 -1
  75. package/dist/{schemaIR-D1MJwqmp.mjs → schemaIR-ajetw3Fv.mjs} +1 -42
  76. package/dist/scim/index.mjs +2 -2
  77. package/dist/sync/changelog.d.mts +2 -0
  78. package/dist/sync/changelog.mjs +2 -0
  79. package/dist/testing/index.d.mts +2 -2
  80. package/dist/testing/index.mjs +2 -2
  81. package/dist/types/index.d.mts +1 -1
  82. package/dist/{types-9_wtNPsI.d.mts → types-BHMKlMBU.d.mts} +53 -195
  83. package/dist/{types-DpvSF5Hh.d.mts → types-C9b2HR5W.d.mts} +1 -1
  84. package/dist/utils/index.d.mts +1 -1
  85. package/dist/utils/index.mjs +1 -1
  86. package/dist/{utils-C6QQ9-hd.mjs → utils-ChjYQpjn.mjs} +1400 -1400
  87. package/dist/{validate-CRcaWzp2.mjs → validate-C2mGgSXg.mjs} +3 -3
  88. package/package.json +13 -8
  89. package/skills/arc/SKILL.md +10 -0
  90. package/skills/arc/references/mcp.md +2 -0
@@ -1,6 +1,8 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { accessSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { accessSync, existsSync, readFileSync } from "node:fs";
4
6
  import * as readline from "node:readline";
5
7
  import { execSync, spawn } from "node:child_process";
6
8
  //#region src/cli/commands/init/templates/adapter.ts
@@ -929,11 +931,32 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
929
931
  }
930
932
  //#endregion
931
933
  //#region src/cli/commands/init/dependency-plan.ts
934
+ /**
935
+ * Single source of truth for scaffolded project dependencies.
936
+ *
937
+ * Two layers keep the scaffold from ever contradicting the arc release
938
+ * that ships it:
939
+ *
940
+ * 1. The static table below — tracks published npm latest, bumped on
941
+ * release. Serves as the offline fallback.
942
+ * 2. A runtime overlay from arc's OWN package.json (version + declared
943
+ * peer floors). The 2.20.0 review found the static pins had drifted
944
+ * (`arc ^2.18.5` + `primitives ^0.6.0` while arc 2.20 peers demand
945
+ * >=0.9.0 → ERESOLVE on the first `arc init` a new user runs).
946
+ * Deriving from the installed package.json makes that class of
947
+ * breakage structurally impossible: the scaffold always pins the
948
+ * arc version actually running and ranges that satisfy its peers.
949
+ *
950
+ * Used by both `packageJsonTemplate` (declares the deps in the generated
951
+ * `package.json` so `npm install` works without a pre-pass) and
952
+ * `installDependencies` (runs the package manager's `install` against
953
+ * the declared ranges). One source — no drift.
954
+ */
932
955
  const SCAFFOLD_DEP_VERSIONS = {
933
956
  core: {
934
- "@classytic/arc": "^2.18.5",
935
- "@classytic/primitives": "^0.6.0",
936
- "@classytic/repo-core": "^0.6.0",
957
+ "@classytic/arc": "^2.20.0",
958
+ "@classytic/primitives": "^0.9.1",
959
+ "@classytic/repo-core": "^0.7.0",
937
960
  "@fastify/cors": "^11.2.0",
938
961
  "@fastify/helmet": "^13.0.2",
939
962
  "@fastify/rate-limit": "^10.3.0",
@@ -970,6 +993,64 @@ const SCAFFOLD_DEP_VERSIONS = {
970
993
  typesJwt: { "@types/bcryptjs": "^3.0.0" }
971
994
  };
972
995
  /**
996
+ * Locate arc's own package.json by walking up from the executing file.
997
+ * A path walk (vs `require.resolve('@classytic/arc/package.json')`) works
998
+ * regardless of bundle chunk layout and of package.json not being in the
999
+ * exports map. Returns undefined when not found — callers fall back to
1000
+ * the static table.
1001
+ */
1002
+ function readOwnPackageJson() {
1003
+ try {
1004
+ let dir = dirname(fileURLToPath(import.meta.url));
1005
+ for (let depth = 0; depth < 6; depth++) {
1006
+ const candidate = join(dir, "package.json");
1007
+ if (existsSync(candidate)) {
1008
+ const parsed = JSON.parse(readFileSync(candidate, "utf8"));
1009
+ if (parsed.name === "@classytic/arc") return parsed;
1010
+ }
1011
+ const parent = dirname(dir);
1012
+ if (parent === dir) break;
1013
+ dir = parent;
1014
+ }
1015
+ } catch {}
1016
+ }
1017
+ /** First x.y.z triple in a range string, or undefined. */
1018
+ function parseFloor(range) {
1019
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(range);
1020
+ if (!m) return void 0;
1021
+ return [
1022
+ Number(m[1]),
1023
+ Number(m[2]),
1024
+ Number(m[3])
1025
+ ];
1026
+ }
1027
+ function floorLessThan(a, b) {
1028
+ if (a[0] !== b[0]) return a[0] < b[0];
1029
+ if (a[1] !== b[1]) return a[1] < b[1];
1030
+ return a[2] < b[2];
1031
+ }
1032
+ /**
1033
+ * Overlay live truth from arc's own package.json onto the static pins:
1034
+ * - `@classytic/arc` pins the exact running version (`^<version>`).
1035
+ * - Any scaffolded dep that is also an arc peer gets raised to `^<peer
1036
+ * floor>` when the static pin's floor is BELOW the declared peer floor
1037
+ * (never lowered — static pins may legitimately be ahead of the floor).
1038
+ */
1039
+ function overlayLiveArcVersions(dependencies) {
1040
+ const own = readOwnPackageJson();
1041
+ if (!own) return;
1042
+ if (own.version && dependencies["@classytic/arc"]) dependencies["@classytic/arc"] = `^${own.version}`;
1043
+ const peers = own.peerDependencies ?? {};
1044
+ for (const [name, staticRange] of Object.entries(dependencies)) {
1045
+ const peerRange = peers[name];
1046
+ if (!peerRange) continue;
1047
+ const peerFloor = parseFloor(peerRange);
1048
+ const staticFloor = parseFloor(staticRange);
1049
+ if (!peerFloor || !staticFloor) continue;
1050
+ if (floorLessThan(staticFloor, peerFloor)) dependencies[name] = `^${peerFloor.join(".")}`;
1051
+ }
1052
+ }
1053
+ /**
973
1054
  * Resolve the dependency manifest for a scaffold configuration.
974
1055
  *
975
1056
  * Returns sorted records (alphabetical by package name) so the generated
@@ -987,6 +1068,7 @@ function resolveScaffoldDependencies(config) {
987
1068
  }
988
1069
  if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
989
1070
  if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
1071
+ overlayLiveArcVersions(dependencies);
990
1072
  return {
991
1073
  dependencies: sortByKey(dependencies),
992
1074
  devDependencies: sortByKey(devDependencies)
@@ -3022,7 +3104,7 @@ async function gatherConfig(options) {
3022
3104
  output: process.stdout
3023
3105
  });
3024
3106
  const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
3025
- const nonInteractive = !!options.name;
3107
+ const nonInteractive = !!options.name || !process.stdin.isTTY;
3026
3108
  try {
3027
3109
  const name = options.name || await question("Project name: ") || "my-arc-app";
3028
3110
  let adapter = options.adapter || "mongokit";
@@ -1,7 +1,7 @@
1
1
  import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-MlYlrYq0.mjs";
2
2
  import { EventGatewayOptions } from "./event-gateway.mjs";
3
3
  import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
4
- import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-DpvSF5Hh.mjs";
4
+ import { c as McpAuthResult, d as PromptDefinition, g as ToolDefinition, h as ToolContext, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpResourceConfig } from "../types-C9b2HR5W.mjs";
5
5
  import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
6
6
  import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
7
7
  export type { BetterAuthHandler, CallToolResult, CreateMcpServerConfig, CrudOperation, EventGatewayOptions, JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, McpAuthResult, McpPluginOptions, McpResourceConfig, PromptDefinition, QueueStats, StreamlinePluginOptions, ToolAnnotations, ToolContext, ToolDefinition, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WorkflowLike, WorkflowRunLike };
@@ -1,7 +1,7 @@
1
- import { V as ResourceDefinition, bt as IControllerResponse, xt as IRequestContext } from "../../index-CUTG2Cuo.mjs";
1
+ import { V as ResourceDefinition, bt as IControllerResponse, xt as IRequestContext } from "../../index-_--GyzcO.mjs";
2
2
  import { i as RequestScope } from "../../types-LNebCoJx.mjs";
3
3
  import { c as PermissionCheck } from "../../fields-C7ahdGYf.mjs";
4
- import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-DpvSF5Hh.mjs";
4
+ import { a as CrudDescriptionOverride, c as McpAuthResult, d as PromptDefinition, f as PromptResult, g as ToolDefinition, h as ToolContext, i as CrudDescriptionMeta, l as McpPluginOptions, m as ToolAnnotations, n as CallToolResult, o as CrudOperation, p as SessionEntry, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpResourceConfig } from "../../types-C9b2HR5W.mjs";
5
5
  import { ErrorContract } from "@classytic/repo-core/errors";
6
6
  import { FastifyPluginAsync } from "fastify";
7
7
  import { z } from "zod";
@@ -1,4 +1,4 @@
1
- import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-C088Nd-G.mjs";
1
+ import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, h as resolveToolCollisions, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-CuW6L_uD.mjs";
2
2
  //#region src/integrations/mcp/defineTool.ts
3
3
  /**
4
4
  * Define a type-safe MCP tool.
@@ -1,4 +1,4 @@
1
- import { c as McpAuthResult, l as McpPluginOptions } from "../../types-DpvSF5Hh.mjs";
1
+ import { c as McpAuthResult, l as McpPluginOptions } from "../../types-C9b2HR5W.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
@@ -1,4 +1,4 @@
1
- import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-C088Nd-G.mjs";
1
+ import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-CuW6L_uD.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -28,8 +28,12 @@ interface WorkflowLike {
28
28
  start(input: unknown, options?: WorkflowStartOptions): Promise<WorkflowRunLike>;
29
29
  execute(runId: string): Promise<WorkflowRunLike>;
30
30
  resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;
31
- cancel(runId: string): Promise<WorkflowRunLike>;
32
- pause?(runId: string): Promise<WorkflowRunLike>;
31
+ cancel(runId: string, options?: {
32
+ reason?: string;
33
+ }): Promise<WorkflowRunLike>;
34
+ pause?(runId: string, options?: {
35
+ reason?: string;
36
+ }): Promise<WorkflowRunLike>;
33
37
  rewindTo?(runId: string, stepId: string): Promise<WorkflowRunLike>;
34
38
  get(runId: string): Promise<WorkflowRunLike | null>;
35
39
  waitFor?(runId: string, options?: {
@@ -39,7 +43,9 @@ interface WorkflowLike {
39
43
  };
40
44
  start(input: unknown, options?: WorkflowStartOptions): Promise<WorkflowRunLike>;
41
45
  resume(runId: string, payload?: unknown): Promise<WorkflowRunLike>;
42
- cancel(runId: string): Promise<WorkflowRunLike>;
46
+ cancel(runId: string, options?: {
47
+ reason?: string;
48
+ }): Promise<WorkflowRunLike>;
43
49
  get(runId: string): Promise<WorkflowRunLike | null>;
44
50
  shutdown?(): void;
45
51
  /** Streamline container for event bridging + repository access (streamline >=2.1) */
@@ -114,6 +120,12 @@ interface WorkflowRunLike {
114
120
  * toward dead-letter.
115
121
  */
116
122
  recoveryAttempts?: number;
123
+ /**
124
+ * Streamline >= 2.7 — operator-supplied cancellation reason, set when
125
+ * `cancel(runId, { reason })` was called. Absent on runs cancelled without
126
+ * a reason or under older streamline. Surfaced so dashboards can show WHY.
127
+ */
128
+ cancellationReason?: string;
117
129
  }
118
130
  /**
119
131
  * Streamline >= 2.3.3 dead-letter discriminator. The run.status stays
@@ -148,7 +160,7 @@ interface StreamlinePluginOptions {
148
160
  * - Step events: started, completed, failed, waiting, skipped,
149
161
  * retry-scheduled, compensated
150
162
  * - Workflow lifecycle: started, completed, failed, waiting, resumed,
151
- * cancelled, recovered, retry, compensating
163
+ * cancelled, recovered, retry, compensating, paused (>= 2.7)
152
164
  * - Engine telemetry: engine:error, scheduler:error,
153
165
  * scheduler:circuit-open
154
166
  *
@@ -231,7 +243,7 @@ interface StreamlinePluginOptions {
231
243
  * adds can be bridged by updating this list; arc never breaks just
232
244
  * because streamline extended its bus.
233
245
  */
234
- declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed", "step:failed", "step:waiting", "step:skipped", "step:retry-scheduled", "step:compensated", "workflow:started", "workflow:completed", "workflow:failed", "workflow:waiting", "workflow:resumed", "workflow:cancelled", "workflow:recovered", "workflow:retry", "workflow:compensating", "engine:error", "scheduler:error", "scheduler:circuit-open"];
246
+ declare const STREAMLINE_BUS_EVENTS: readonly ["step:started", "step:completed", "step:failed", "step:waiting", "step:skipped", "step:retry-scheduled", "step:compensated", "workflow:started", "workflow:completed", "workflow:failed", "workflow:waiting", "workflow:resumed", "workflow:cancelled", "workflow:recovered", "workflow:retry", "workflow:compensating", "workflow:paused", "engine:error", "scheduler:error", "scheduler:circuit-open"];
235
247
  /**
236
248
  * Non-durable streaming frames from streamline >= 2.6's `ctx.stream(frame)`
237
249
  * (LLM tokens, percent-complete, live previews).
@@ -45,6 +45,7 @@ const STREAMLINE_BUS_EVENTS = [
45
45
  "workflow:recovered",
46
46
  "workflow:retry",
47
47
  "workflow:compensating",
48
+ "workflow:paused",
48
49
  "engine:error",
49
50
  "scheduler:error",
50
51
  "scheduler:circuit-open"
@@ -80,7 +81,7 @@ const STREAMLINE_TERMINAL_EVENTS = [
80
81
  * emit no further bus events, so the SSE handler sends its snapshot and
81
82
  * closes immediately rather than holding a connection open on a dead run.
82
83
  */
83
- const TERMINAL_RUN_STATUSES = new Set([
84
+ const TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set([
84
85
  "done",
85
86
  "failed",
86
87
  "cancelled"
@@ -130,7 +131,7 @@ const streamlinePluginImpl = async (fastify, options) => {
130
131
  code: "arc.streamline.invalid_body",
131
132
  workflowId: id
132
133
  });
133
- const envelopeKeys = new Set([
134
+ const envelopeKeys = /* @__PURE__ */ new Set([
134
135
  "input",
135
136
  "meta",
136
137
  "idempotencyKey",
@@ -223,11 +224,13 @@ const streamlinePluginImpl = async (fastify, options) => {
223
224
  if (!await checkPerm("cancel", request)) throw new ForbiddenError();
224
225
  const { runId } = request.params;
225
226
  await assertRunVisible(request, runId);
226
- const run = await wf.cancel(runId);
227
+ const { reason: cancelReason } = request.body ?? {};
228
+ const run = await wf.cancel(runId, cancelReason !== void 0 ? { reason: cancelReason } : void 0);
227
229
  if (bridgeEvents && fastify.events?.publish) try {
228
230
  await fastify.events.publish(`workflow.${id}.cancelled`, {
229
231
  runId: run._id,
230
- workflowId: id
232
+ workflowId: id,
233
+ ...cancelReason !== void 0 ? { reason: cancelReason } : {}
231
234
  });
232
235
  } catch (err) {
233
236
  fastify.log.warn({
@@ -271,7 +274,8 @@ const streamlinePluginImpl = async (fastify, options) => {
271
274
  if (wf.engine.pause) fastify.post(`${routePrefix}/runs/:runId/pause`, { preHandler: authPreHandler }, async (request, _reply) => {
272
275
  const { runId } = request.params;
273
276
  await assertRunVisible(request, runId);
274
- return await wf.engine.pause?.(runId);
277
+ const { reason: pauseReason } = request.body ?? {};
278
+ return await wf.engine.pause?.(runId, pauseReason !== void 0 ? { reason: pauseReason } : void 0);
275
279
  });
276
280
  if (wf.engine.rewindTo) fastify.post(`${routePrefix}/runs/:runId/rewind`, { preHandler: authPreHandler }, async (request, _reply) => {
277
281
  const { runId } = request.params;
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "./ResourceRegistry-5fKZwhK_.mjs";
1
+ import { t as ResourceRegistry } from "./ResourceRegistry-7RnrEWNP.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/utils/loadResourcesFromEntry.ts
@@ -1,14 +1,15 @@
1
1
  import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
2
- import { t as BaseController } from "./BaseController-Dy6EQHjM.mjs";
3
- import { N as normalizePermissionResult } from "./permissions-BPB1xqGf.mjs";
4
- import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-D7NGwcAR.mjs";
2
+ import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
3
+ import { t as BaseController } from "./BaseController-D-MHZ27_.mjs";
4
+ import { N as normalizePermissionResult } from "./permissions-CgbOLJZG.mjs";
5
5
  import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
6
- import { t as pluralize } from "./pluralize-DccFV9R7.mjs";
7
- import { i as shouldRejectAdditionalProperties, r as schemaIRToZodShape, t as normalizeSchemaIR } from "./schemaIR-D1MJwqmp.mjs";
8
- import { n as validateAggregations } from "./validate-CRcaWzp2.mjs";
9
- import { n as executeAggregation } from "./buildHandler-B0qvAguf.mjs";
10
- import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
6
+ import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-C9RMiIpc.mjs";
7
+ import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
8
+ import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
9
+ import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
10
+ import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-ajetw3Fv.mjs";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
+ import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
12
13
  import fp from "fastify-plugin";
13
14
  import { z } from "zod";
14
15
  //#region src/integrations/mcp/createMcpServer.ts
@@ -264,7 +265,7 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
264
265
  * them explicitly here rather than asking the parser at runtime — Arc must
265
266
  * not import MongoKit internals just to know what an operator looks like.
266
267
  */
267
- const OPERATOR_SUFFIXES = new Set([
268
+ const OPERATOR_SUFFIXES = /* @__PURE__ */ new Set([
268
269
  "eq",
269
270
  "ne",
270
271
  "gt",
@@ -671,12 +672,20 @@ function resolveCrudDescription(override, meta) {
671
672
  * // shape = { name: z.string(), price: z.number() }
672
673
  * ```
673
674
  */
675
+ /**
676
+ * Projection (`select`) field — reused by the list shape AND the single-entity
677
+ * `get` shape. The controller honors `select` on `getById` identically to list
678
+ * (both flow through `QueryResolver.resolve`), so exposing it on `get` gives an
679
+ * MCP agent the same field-projection it already has on list.
680
+ */
681
+ const PROJECTION_FIELD = z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description').");
674
682
  const PAGINATION_SHAPE = {
675
- page: z.number().int().min(1).optional().describe("Page number (1-based)"),
683
+ page: z.number().int().min(1).optional().describe("Page number (1-based). Ignored when `cursor` is set."),
676
684
  limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
685
+ cursor: z.string().optional().describe("Opaque keyset cursor from a previous page's `next`. Pass it to fetch the following page; when set, `page` is ignored (cursor/keyset pagination — stable under concurrent inserts)."),
677
686
  sort: z.string().optional().describe("Sort field, prefix with - for descending"),
678
687
  search: z.string().optional().describe("Full-text search query"),
679
- select: z.string().optional().describe("Comma-separated field list to project (e.g. 'name,price'). Prefix with '-' to exclude (e.g. '-description')."),
688
+ select: PROJECTION_FIELD,
680
689
  populate: z.string().optional().describe("Comma-separated relation paths to hydrate (e.g. 'supplier,category'). Follows Mongoose populate syntax when the adapter is MongoKit.")
681
690
  };
682
691
  /**
@@ -688,7 +697,7 @@ function fieldRulesToZod(fieldRules, options = {}) {
688
697
  const { mode = "create", hiddenFields = [], readonlyFields = [], extraHideFields = [] } = options;
689
698
  if (mode === "list") return buildListShape(fieldRules, options);
690
699
  if (!fieldRules) return {};
691
- const allHidden = new Set([...hiddenFields, ...extraHideFields]);
700
+ const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
692
701
  const allReadonly = new Set(readonlyFields);
693
702
  const shape = {};
694
703
  for (const [name, rule] of Object.entries(fieldRules)) {
@@ -738,7 +747,7 @@ function typeToZod(type) {
738
747
  }
739
748
  }
740
749
  /** Operators that apply to numeric/date fields */
741
- const COMPARISON_OPS = new Set([
750
+ const COMPARISON_OPS = /* @__PURE__ */ new Set([
742
751
  "gt",
743
752
  "gte",
744
753
  "lt",
@@ -761,7 +770,7 @@ function opDescription(op, fieldName) {
761
770
  /** Build list/query shape with filterable fields, operators, and pagination */
762
771
  function buildListShape(fieldRules, options) {
763
772
  const { filterableFields = [], hiddenFields = [], extraHideFields = [], allowedOperators } = options;
764
- const allHidden = new Set([...hiddenFields, ...extraHideFields]);
773
+ const allHidden = /* @__PURE__ */ new Set([...hiddenFields, ...extraHideFields]);
765
774
  const shape = { ...PAGINATION_SHAPE };
766
775
  if (!fieldRules) return shape;
767
776
  for (const name of filterableFields) {
@@ -917,6 +926,61 @@ async function forwardResponse(reply, response) {
917
926
  reply.send(await response.text());
918
927
  }
919
928
  //#endregion
929
+ //#region src/core/schemaIRZod.ts
930
+ /**
931
+ * Schema IR → Zod adapter — the MCP-facing half of [schemaIR.ts](./schemaIR.ts).
932
+ *
933
+ * Lives in its OWN module (not schemaIR.ts) because it is the only part that
934
+ * needs zod at runtime. schemaIR.ts is statically imported by
935
+ * `createActionRouter`, which every actions-bearing resource loads at boot —
936
+ * a top-level `import { z } from "zod"` there made zod a de-facto REQUIRED
937
+ * dependency for any host using `actions:`, despite zod being an optional
938
+ * peer (boot crash when absent, even with plain-JSON-Schema action bodies
939
+ * and no MCP). Only MCP code imports this module, and the MCP integration
940
+ * already hard-requires zod (the SDK does), so the top-level import is
941
+ * correct HERE and a bug THERE.
942
+ */
943
+ /**
944
+ * Emit a flat Zod shape from the IR. The MCP SDK wraps the returned record
945
+ * in `z.object()` internally, so we return the bare shape (same contract
946
+ * as `ToolDefinition.inputSchema`).
947
+ *
948
+ * `additionalProperties: false` is honored at the MCP handler layer rather
949
+ * than baked into the Zod shape — the SDK's input validation happens before
950
+ * the handler runs, and flat shapes can't express `.strict()` mode.
951
+ * `shouldRejectAdditionalProperties(ir)` (schemaIR.ts) returns the flag so
952
+ * callers can gate their handler on it.
953
+ */
954
+ function schemaIRToZodShape(ir) {
955
+ const requiredSet = new Set(ir.required);
956
+ const result = {};
957
+ for (const [name, prop] of Object.entries(ir.properties)) {
958
+ const desc = typeof prop.description === "string" && prop.description.length > 0 ? prop.description : name;
959
+ const base = jsonSchemaPropToZod(prop);
960
+ result[name] = requiredSet.has(name) ? base.describe(desc) : base.optional().describe(desc);
961
+ }
962
+ return result;
963
+ }
964
+ /**
965
+ * Convert a single JSON Schema property to a Zod type. Understands enum,
966
+ * numeric/integer/boolean/array/object, and falls back to string for
967
+ * unrecognized types (matches MCP's "strings for opaque fields" convention).
968
+ *
969
+ * Internal — use `schemaIRToZodShape` which wires this up with required/optional
970
+ * + description handling.
971
+ */
972
+ function jsonSchemaPropToZod(schema) {
973
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return z.enum(schema.enum);
974
+ switch (typeof schema.type === "string" ? schema.type : "string") {
975
+ case "number":
976
+ case "integer": return z.number();
977
+ case "boolean": return z.boolean();
978
+ case "array": return z.array(z.unknown());
979
+ case "object": return z.record(z.string(), z.unknown());
980
+ default: return z.string();
981
+ }
982
+ }
983
+ //#endregion
920
984
  //#region src/integrations/mcp/action-tools.ts
921
985
  /**
922
986
  * Convert an action's `schema` field into a Zod shape for MCP input.
@@ -942,7 +1006,7 @@ function convertActionSchemaToZod(raw) {
942
1006
  */
943
1007
  function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
944
1008
  const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
945
- const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
1009
+ const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
946
1010
  return async (input, ctx) => {
947
1011
  const session = ctx.session;
948
1012
  if (allowedKeys) {
@@ -1239,7 +1303,10 @@ function buildInputSchema(op, fieldRules, opts) {
1239
1303
  mode: "list",
1240
1304
  ...opts
1241
1305
  });
1242
- case "get": return getIdShape();
1306
+ case "get": return {
1307
+ ...getIdShape(),
1308
+ select: PROJECTION_FIELD
1309
+ };
1243
1310
  case "create":
1244
1311
  if (!fieldRules && opts.adapterBodies?.createBody) {
1245
1312
  const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
@@ -1424,14 +1491,17 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1424
1491
  const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
1425
1492
  if (typeof route.handler === "function") {
1426
1493
  const fn = route.handler;
1427
- if (pipelineSteps.length > 0) return toCallToolResult(await executePipeline(pipelineSteps, {
1428
- ...reqCtx,
1429
- resource: resourceName,
1430
- operation: operationName
1431
- }, async (ctx) => {
1432
- const raw = await fn(ctx);
1433
- return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1434
- }, operationName));
1494
+ if (pipelineSteps.length > 0) {
1495
+ const pipeCtx = {
1496
+ ...reqCtx,
1497
+ resource: resourceName,
1498
+ operation: operationName
1499
+ };
1500
+ return toCallToolResult(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
1501
+ const raw = await fn(ctx);
1502
+ return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
1503
+ }, operationName));
1504
+ }
1435
1505
  const out = await fn(reqCtx);
1436
1506
  return toCallToolResult(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
1437
1507
  }
@@ -1452,13 +1522,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
1452
1522
  };
1453
1523
  return toCallToolResult(await method(reqCtx));
1454
1524
  } catch (err) {
1455
- return {
1456
- content: [{
1457
- type: "text",
1458
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
1459
- }],
1460
- isError: true
1461
- };
1525
+ return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1462
1526
  }
1463
1527
  };
1464
1528
  }
@@ -1472,13 +1536,7 @@ function createMcpHandlerPassthrough(mcpHandler) {
1472
1536
  try {
1473
1537
  return await mcpHandler(input);
1474
1538
  } catch (err) {
1475
- return {
1476
- content: [{
1477
- type: "text",
1478
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
1479
- }],
1480
- isError: true
1481
- };
1539
+ return toCallToolError(err instanceof Error ? err : new Error(String(err)));
1482
1540
  }
1483
1541
  };
1484
1542
  }
@@ -1655,14 +1713,18 @@ function resourceToTools(resource, config = {}) {
1655
1713
  if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
1656
1714
  const repoForAgg = resource.controller?.repository;
1657
1715
  const buildOptionsFromSession = (session) => {
1658
- const s = session;
1716
+ const auth = session ?? null;
1717
+ const scope = buildScope(auth);
1659
1718
  const out = {};
1660
- const orgId = s?.scope?.organizationId;
1719
+ const orgId = getOrgId(scope);
1661
1720
  if (orgId) out.organizationId = orgId;
1662
- const userId = s?.scope?.userId ?? s?.user?.id;
1721
+ const userId = getUserId(scope);
1663
1722
  if (userId) out.userId = userId;
1664
- if (s?.user) out.user = s.user;
1665
- if (s?.requestId) out.requestId = s.requestId;
1723
+ if (auth?.userId) out.user = {
1724
+ id: auth.userId,
1725
+ roles: auth.roles ?? [],
1726
+ orgRoles: auth.orgRoles ?? []
1727
+ };
1666
1728
  return out;
1667
1729
  };
1668
1730
  tools.push(...buildAggregationTools({
@@ -1713,13 +1775,29 @@ function registerSchemaResources(server, resources, overrides) {
1713
1775
  name: r.name,
1714
1776
  displayName: r.displayName,
1715
1777
  operations: getOps(r, overrides?.[r.name]?.operations),
1716
- fields: r.schemaOptions?.fieldRules ?? {},
1778
+ fields: visibleFieldRules(r.schemaOptions?.fieldRules),
1717
1779
  filterableFields: schemaOpts?.filterableFields ?? [],
1718
1780
  presets: r._appliedPresets ?? []
1719
1781
  }, null, 2)
1720
1782
  }] }));
1721
1783
  }
1722
1784
  }
1785
+ /**
1786
+ * Strip `hidden: true` / `systemManaged: true` entries before serializing —
1787
+ * the schema-discovery resource must not re-expose exactly what
1788
+ * `fieldRulesToZod` strips from tool input schemas (tenant keys, internal
1789
+ * scoring fields, ...). Same visibility rule, one discovery surface.
1790
+ */
1791
+ function visibleFieldRules(fieldRules) {
1792
+ if (!fieldRules) return {};
1793
+ const out = {};
1794
+ for (const [name, rule] of Object.entries(fieldRules)) {
1795
+ const flags = rule ?? {};
1796
+ if (flags.hidden === true || flags.systemManaged === true) continue;
1797
+ out[name] = rule;
1798
+ }
1799
+ return out;
1800
+ }
1723
1801
  function getOps(r, override) {
1724
1802
  let ops = [
1725
1803
  "list",
@@ -1901,10 +1979,16 @@ const mcpPluginImpl = async (fastify, options) => {
1901
1979
  const more = allTools.length > 5 ? ` (+${allTools.length - 5} more)` : "";
1902
1980
  fastify.log.warn(`[arc/mcp] MCP server registered at "${prefix}" with auth DISABLED. Anyone who can reach this URL has full access to ${allTools.length} tools across ${enabledResources.length} resources: ${toolList}${more}. Pass \`auth: betterAuthInstance\` or \`auth: async (headers) => ...\` to gate access. Safe only for stdio transports, local development, or explicitly-public read-only APIs.`);
1903
1981
  }
1904
- fastify.get(`${prefix}/health`, async (_request, reply) => {
1905
- reply.send({
1982
+ fastify.get(`${prefix}/health`, async (request, reply) => {
1983
+ const liveness = {
1906
1984
  status: "ok",
1907
- mode: stateful ? "stateful" : "stateless",
1985
+ mode: stateful ? "stateful" : "stateless"
1986
+ };
1987
+ if (options.auth) {
1988
+ if (!await resolveMcpAuth(request.headers, options.auth)) return reply.send(liveness);
1989
+ }
1990
+ reply.send({
1991
+ ...liveness,
1908
1992
  tools: allTools.length,
1909
1993
  resources: enabledResources.length,
1910
1994
  toolNames: allTools.map((t) => t.name),
@@ -1991,9 +2075,22 @@ function registerStatelessRoutes(fastify, prefix, options, createServer, Transpo
1991
2075
  });
1992
2076
  }
1993
2077
  function registerStatefulRoutes(fastify, prefix, options, cache, createServer, Transport) {
1994
- /** Check if the requesting principal owns the session */
2078
+ /**
2079
+ * Check if the requesting principal owns the session. FAIL-CLOSED:
2080
+ *
2081
+ * - `authResult === null` (failed/absent credentials) NEVER owns a
2082
+ * session — pre-2.20 this returned `true`, so an unauthenticated
2083
+ * caller with a leaked `Mcp-Session-Id` could attach to the victim's
2084
+ * SSE stream, terminate the session, and (via the auth-snapshot
2085
+ * refresh) wipe `entry.auth` to null — after which ANY principal
2086
+ * passed ownership and took over the session.
2087
+ * - `entry.auth === null` under configured auth is equally a deny:
2088
+ * POST 401s before creating a session, so an auth-mode session always
2089
+ * has an owner snapshot; a missing one means corrupted state.
2090
+ */
1995
2091
  function isSessionOwner(entry, authResult) {
1996
- if (!options.auth || !entry.auth || !authResult) return true;
2092
+ if (!options.auth) return true;
2093
+ if (!authResult || !entry.auth) return false;
1997
2094
  const prev = entry.auth;
1998
2095
  return prev.userId === authResult.userId && prev.organizationId === authResult.organizationId && prev.clientId === authResult.clientId;
1999
2096
  }
@@ -2053,6 +2150,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2053
2150
  if (!entry) return reply.code(403).send({ error: "Unauthorized" });
2054
2151
  if (options.auth) {
2055
2152
  const authResult = await resolveMcpAuth(request.headers, options.auth);
2153
+ if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
2056
2154
  if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
2057
2155
  entry.auth = authResult;
2058
2156
  entry.authRef.current = authResult;
@@ -2067,6 +2165,7 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
2067
2165
  if (!entry) return reply.code(204).send();
2068
2166
  if (options.auth) {
2069
2167
  const authResult = await resolveMcpAuth(request.headers, options.auth);
2168
+ if (!authResult) return reply.code(401).send({ error: "Unauthorized" });
2070
2169
  if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
2071
2170
  entry.auth = authResult;
2072
2171
  entry.authRef.current = authResult;
@@ -1,4 +1,4 @@
1
- import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-CUTG2Cuo.mjs";
1
+ import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-_--GyzcO.mjs";
2
2
  import { RouteHandlerMethod } from "fastify";
3
3
 
4
4
  //#region src/middleware/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { t as CRUD_OPERATIONS } from "../constants-gaEeMlz-.mjs";
1
+ import { t as CRUD_OPERATIONS } from "../constants-DNre0wRg.mjs";
2
2
  import { t as multipartBody } from "../multipartBody-CHEQSZ6G.mjs";
3
3
  //#region src/middleware/middleware.ts
4
4
  /**