@classytic/arc 2.21.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 (115) hide show
  1. package/dist/{BaseController-BnLYgE9A.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 +2 -2
  6. package/dist/auth/index.d.mts +30 -2
  7. package/dist/auth/index.mjs +6 -3
  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-DZ6Xe1jR.mjs → core-DQ6rDTyH.mjs} +126 -68
  17. package/dist/{createActionRouter-BWMDW-dL.mjs → createActionRouter-BuOv-m4Y.mjs} +10 -6
  18. package/dist/{createAggregationRouter-l5mNdYrZ.mjs → createAggregationRouter-CgJv_BU_.mjs} +3 -3
  19. package/dist/{createApp-Bi61XxqU.mjs → createApp-CjOeWJ9_.mjs} +252 -57
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/encryption/index.mjs +9 -5
  23. package/dist/{errorHandler-Dxf6P_kV.mjs → errorHandler-Bq2OeEQL.mjs} +34 -10
  24. package/dist/{errorHandler-B0e-bq-O.d.mts → errorHandler-C6yv4ZTW.d.mts} +13 -0
  25. package/dist/events/index.mjs +5 -2
  26. package/dist/factory/index.d.mts +46 -9
  27. package/dist/factory/index.mjs +64 -13
  28. package/dist/{fastify-augmentation-DafigGQp.d.mts → fastify-augmentation-g_f9omSc.d.mts} +2 -2
  29. package/dist/fields-D3lhgzhD.d.mts +121 -0
  30. package/dist/health-ScyjIVo5.mjs +202 -0
  31. package/dist/hooks/index.d.mts +1 -1
  32. package/dist/idempotency/index.d.mts +7 -0
  33. package/dist/idempotency/index.mjs +7 -17
  34. package/dist/{index-Dtrj6XZk.d.mts → index-BBsvQt7U.d.mts} +1310 -1226
  35. package/dist/{types-DpxoNX5v.d.mts → index-Ba10fdKp.d.mts} +196 -66
  36. package/dist/{index-B5N-NaHs.d.mts → index-CC5ruDGh.d.mts} +3 -3
  37. package/dist/{index-B5PoNbfu.d.mts → index-wMoWBLkn.d.mts} +68 -2
  38. package/dist/index.d.mts +6 -5
  39. package/dist/index.mjs +6 -5
  40. package/dist/{init-0YhHkXGk.mjs → init-CjO4p_q1.mjs} +123 -4
  41. package/dist/integrations/event-gateway.mjs +1 -1
  42. package/dist/integrations/index.d.mts +1 -1
  43. package/dist/integrations/mcp/index.d.mts +67 -4
  44. package/dist/integrations/mcp/index.mjs +1 -1
  45. package/dist/integrations/mcp/testing.d.mts +1 -1
  46. package/dist/integrations/mcp/testing.mjs +1 -1
  47. package/dist/integrations/webhooks.d.mts +131 -7
  48. package/dist/integrations/webhooks.mjs +169 -10
  49. package/dist/interface-n5hpKg1d.mjs +16 -0
  50. package/dist/jsonBody-BlzAoGCe.mjs +43 -0
  51. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  52. package/dist/logger/index.d.mts +26 -1
  53. package/dist/logger/index.mjs +39 -1
  54. package/dist/{mcpPlugin-B9XjjNVG.mjs → mcpPlugin-BtzIL_kM.mjs} +312 -39
  55. package/dist/middleware/index.d.mts +1 -1
  56. package/dist/{module-WeuU5jfd.d.mts → module-B4u10iOJ.d.mts} +26 -1
  57. package/dist/{openapi-Kyod_di6.mjs → openapi-jSDx-VDv.mjs} +3 -3
  58. package/dist/permissions/index.d.mts +4 -3
  59. package/dist/permissions/index.mjs +3 -2
  60. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  61. package/dist/pipeline/index.d.mts +1 -1
  62. package/dist/plugins/index.d.mts +24 -7
  63. package/dist/plugins/index.mjs +14 -282
  64. package/dist/plugins/response-cache.mjs +1 -0
  65. package/dist/plugins/tracing-entry.mjs +7 -4
  66. package/dist/presets/filesUpload.d.mts +2 -2
  67. package/dist/presets/filesUpload.mjs +1 -1
  68. package/dist/presets/index.d.mts +1 -1
  69. package/dist/presets/index.mjs +1 -1
  70. package/dist/presets/multiTenant.d.mts +1 -1
  71. package/dist/presets/search.d.mts +2 -2
  72. package/dist/presets/search.mjs +1 -1
  73. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  74. package/dist/rateLimitKey-BoWRH3e7.mjs +14 -0
  75. package/dist/realtime-I8OrfnMR.mjs +375 -0
  76. package/dist/{registerRoute-oGAl0oA8.mjs → registerRoute-CWO6mdYe.mjs} +38 -18
  77. package/dist/registry/index.d.mts +2 -2
  78. package/dist/registry/index.mjs +1 -1
  79. package/dist/requestId-5BhTVzBv.mjs +113 -0
  80. package/dist/schemaConverter-DJvodiHj.mjs +171 -0
  81. package/dist/{schemaIR-ajetw3Fv.mjs → schemaIR-Ca7ySNPx.mjs} +2 -2
  82. package/dist/scim/index.mjs +6 -7
  83. package/dist/scope/index.mjs +2 -76
  84. package/dist/scope-LONQ3mfZ.mjs +37 -0
  85. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  86. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  87. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  88. package/dist/testing/index.d.mts +83 -3
  89. package/dist/testing/index.mjs +70 -3
  90. package/dist/types/index.d.mts +2 -2
  91. package/dist/{types-DRj-Ut4n.d.mts → types-5OuqeTDX.d.mts} +37 -2
  92. package/dist/{fields-BGqrAa_u.d.mts → types-Csxga7d3.d.mts} +1 -121
  93. package/dist/usage/index.d.mts +142 -0
  94. package/dist/usage/index.mjs +0 -0
  95. package/dist/utils/index.d.mts +101 -19
  96. package/dist/utils/index.mjs +5 -3
  97. package/dist/{utils-CO8a3B_1.mjs → utils-CZyeRXTh.mjs} +80 -82
  98. package/dist/{versioning-DQ4mCvg0.d.mts → versioning-D6PvCwpq.d.mts} +43 -9
  99. package/llms.txt +39 -0
  100. package/package.json +513 -447
  101. package/skills/arc/SKILL.md +14 -8
  102. package/skills/arc/references/api-reference.md +34 -0
  103. package/skills/arc/references/auth.md +21 -4
  104. package/skills/arc/references/mcp.md +1 -1
  105. package/skills/arc/references/production.md +734 -654
  106. package/skills/arc-code-review/SKILL.md +1 -1
  107. package/skills/arc-code-review/references/anti-patterns.md +71 -18
  108. package/skills/arc-code-review/references/arc-cheatsheet.md +15 -2
  109. package/skills/arc-code-review/references/migration-recipes.md +7 -7
  110. package/skills/arc-code-review/references/mongokit-migration.md +4 -3
  111. package/skills/arc-module-publishing/SKILL.md +2 -1
  112. package/skills/arc-module-publishing/references/authoring-modules.md +55 -0
  113. package/skills/arc-module-publishing/references/package-manifest.md +4 -2
  114. package/dist/preloadResources-CHw3_oA_.mjs +0 -45
  115. package/dist/schemaConverter-U0MnO9vd.mjs +0 -110
@@ -1,3 +1,4 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
1
2
  import * as fs from "node:fs/promises";
2
3
  import * as path from "node:path";
3
4
  import { dirname, join } from "node:path";
@@ -76,6 +77,60 @@ export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
76
77
  `;
77
78
  }
78
79
  //#endregion
80
+ //#region src/cli/commands/init/templates/agents.ts
81
+ function agentsGuideTemplate(config) {
82
+ return `# CLAUDE.md — agent guide for this app
83
+
84
+ > This file doubles as AGENTS.md — copy or symlink if your tool reads that name.
85
+
86
+ **This is an \`@classytic/arc\` application.** Resources are DECLARED, not hand-rolled:
87
+ one \`defineResource()\` = REST + validation + auth + permissions + events + caching +
88
+ OpenAPI + MCP tools. Data layer: ${config.adapter === "mongokit" ? "@classytic/mongokit (Mongoose)" : "a custom DataAdapter kit"}.
89
+
90
+ ## The one rule
91
+
92
+ **Hand-rolling is a bug.** Before writing ANY route, validation, pagination, auth check,
93
+ cron, counter, or audit code by hand, prove arc lacks it — check the table below, then
94
+ \`node_modules/@classytic/arc/skills/arc/SKILL.md\` (full framework guide + references).
95
+
96
+ ## About to write X? Arc already has Y
97
+
98
+ | If you're about to write… | Use instead |
99
+ |---|---|
100
+ | \`fastify.get/post/...\` CRUD routes for an entity | \`defineResource({ name, adapter, permissions })\` |
101
+ | \`if (req.user.role !== 'admin') return 403\` | \`permissions: { update: requireRoles(['admin']) }\` |
102
+ | Reading \`req.user._id\` / org id directly | \`getUserId(scope)\` / \`requireOrgId(scope)\` from \`@classytic/arc/scope\` |
103
+ | Hand-written \`schema: { body }\` / JSON Schema | \`schemaOptions.fieldRules\` (or \`customSchemas\` for wire≠model) |
104
+ | toJSON transforms hiding \`password\`/internals | \`fieldRules: { password: { hidden: true } }\` |
105
+ | Manual \`req.query\` filter/sort/pagination parsing | the resource's \`queryParser\` (allow-listed fields) |
106
+ | Soft delete / slug lookup / tree / bulk / file upload | \`presets: ['softDelete', 'slugLookup', ...]\` |
107
+ | \`setInterval\` sweeps + hand-rolled leader lock | \`schedulesPlugin\` (\`@classytic/arc/plugins\`) |
108
+ | A background/queue worker entrypoint | \`createWorker(sameOptions)\` (\`@classytic/arc/factory\`) |
109
+ | Usage counters / quotas / plan rate limits | \`usagePlugin\` + \`requireQuota\` + \`rateLimit.plan\` |
110
+ | Audit log rows / "who changed what" endpoints | \`audit: true\` + \`history: true\` on the resource |
111
+ | Manual idempotency keys / dedupe | \`idempotencyPlugin\` |
112
+ | Event emitters / webhooks plumbing | \`eventPlugin\` + resource \`events:\` (+ outbox for guarantees) |
113
+ | Custom MCP/AI tool handlers | generated automatically from every resource |
114
+
115
+ ## Load-bearing conventions
116
+
117
+ - \`request.user\` is \`undefined\` on public routes — always guard.
118
+ - Never read \`scope.organizationId\` directly — use \`@classytic/arc/scope\` accessors.
119
+ - The DB connection belongs in \`createApp({ beforeBoot })\` — not before \`createApp\`.
120
+ - Custom endpoints: \`routes:\`/\`actions:\` on the resource (they inherit auth/permissions/
121
+ OpenAPI/MCP) — a bare \`fastify.get()\` gets none of that.
122
+
123
+ ## Commands
124
+
125
+ \`\`\`bash
126
+ npm run dev # watch mode
127
+ npm test # vitest
128
+ npx arc generate resource <name> # scaffold a resource (model/repo/resource files)
129
+ npx arc describe # list registered resources + routes
130
+ \`\`\`
131
+ `;
132
+ }
133
+ //#endregion
79
134
  //#region src/cli/commands/init/templates/app.ts
80
135
  function indexTemplate(config) {
81
136
  const ts = config.typescript;
@@ -442,8 +497,8 @@ function betterAuthSetupTemplate(config) {
442
497
  import { mongodbAdapter } from '@better-auth/mongo-adapter';` : "";
443
498
  const stubsImport = useStubs ? `import { registerBetterAuthStubs } from '@classytic/mongokit/better-auth';` : "";
444
499
  const pluginImports = [
445
- useOrg ? `import { organization } from 'better-auth/plugins';` : "",
446
- useBearer ? `import { bearer } from 'better-auth/plugins';` : "",
500
+ useOrg ? `import { organization } from 'better-auth/plugins/organization';` : "",
501
+ useBearer ? `import { bearer } from 'better-auth/plugins/bearer';` : "",
447
502
  useApiKey ? `import { apiKey } from '@better-auth/api-key';` : ""
448
503
  ].filter(Boolean).join("\n");
449
504
  const dbAdapter = useMongo ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as unknown as never),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
@@ -985,6 +1040,11 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
985
1040
  * `installDependencies` (runs the package manager's `install` against
986
1041
  * the declared ranges). One source — no drift.
987
1042
  */
1043
+ var dependency_plan_exports = /* @__PURE__ */ __exportAll({
1044
+ SCAFFOLD_DEP_VERSIONS: () => SCAFFOLD_DEP_VERSIONS,
1045
+ primeLatestScaffoldVersions: () => primeLatestScaffoldVersions,
1046
+ resolveScaffoldDependencies: () => resolveScaffoldDependencies
1047
+ });
988
1048
  const SCAFFOLD_DEP_VERSIONS = {
989
1049
  core: {
990
1050
  "@classytic/arc": "^2.20.0",
@@ -1014,10 +1074,10 @@ const SCAFFOLD_DEP_VERSIONS = {
1014
1074
  },
1015
1075
  devCommon: {
1016
1076
  "@biomejs/biome": "^2.4.15",
1017
- "mongodb-memory-server": "^11.1.0",
1018
1077
  "pino-pretty": "^13.1.3",
1019
1078
  vitest: "^4.1.7"
1020
1079
  },
1080
+ devMongokit: { "mongodb-memory-server": "^11.1.0" },
1021
1081
  devTypescript: {
1022
1082
  "@types/node": "^22.10.0",
1023
1083
  tsx: "^4.22.3",
@@ -1069,6 +1129,51 @@ function floorLessThan(a, b) {
1069
1129
  * floor>` when the static pin's floor is BELOW the declared peer floor
1070
1130
  * (never lowered — static pins may legitimately be ahead of the floor).
1071
1131
  */
1132
+ const latestCache = /* @__PURE__ */ new Map();
1133
+ function allScaffoldPackageNames() {
1134
+ const names = /* @__PURE__ */ new Set();
1135
+ for (const group of Object.values(SCAFFOLD_DEP_VERSIONS)) for (const name of Object.keys(group)) names.add(name);
1136
+ return [...names];
1137
+ }
1138
+ async function primeLatestScaffoldVersions(options) {
1139
+ const timeoutMs = options?.timeoutMs ?? 3500;
1140
+ const registry = (options?.registry ?? process.env.npm_config_registry ?? "https://registry.npmjs.org").replace(/\/$/, "");
1141
+ const names = allScaffoldPackageNames();
1142
+ const results = await Promise.allSettled(names.map(async (name) => {
1143
+ const res = await fetch(`${registry}/${name}/latest`, {
1144
+ signal: AbortSignal.timeout(timeoutMs),
1145
+ headers: { accept: "application/json" }
1146
+ });
1147
+ if (!res.ok) throw new Error(String(res.status));
1148
+ const body = await res.json();
1149
+ if (typeof body.version === "string" && /^\d+\.\d+\.\d+$/.test(body.version)) latestCache.set(name, body.version);
1150
+ }));
1151
+ return {
1152
+ resolved: latestCache.size,
1153
+ total: results.length
1154
+ };
1155
+ }
1156
+ function parseBareVersion(v) {
1157
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v);
1158
+ return m ? [
1159
+ Number(m[1]),
1160
+ Number(m[2]),
1161
+ Number(m[3])
1162
+ ] : void 0;
1163
+ }
1164
+ function applyLatestOverlay(dependencies) {
1165
+ if (latestCache.size === 0) return;
1166
+ for (const [name, currentRange] of Object.entries(dependencies)) {
1167
+ const latest = latestCache.get(name);
1168
+ if (!latest || latest.includes("-")) continue;
1169
+ const latestV = parseBareVersion(latest);
1170
+ const currentFloor = parseFloor(currentRange);
1171
+ if (!latestV || !currentFloor) continue;
1172
+ if (!floorLessThan(currentFloor, latestV)) continue;
1173
+ if (latestV[0] !== currentFloor[0] && !name.startsWith("@classytic/")) continue;
1174
+ dependencies[name] = `^${latest}`;
1175
+ }
1176
+ }
1072
1177
  function overlayLiveArcVersions(dependencies) {
1073
1178
  const own = readOwnPackageJson();
1074
1179
  if (!own) return;
@@ -1100,8 +1205,13 @@ function resolveScaffoldDependencies(config) {
1100
1205
  Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
1101
1206
  if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.typesJwt);
1102
1207
  }
1103
- if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
1208
+ if (config.adapter === "mongokit") {
1209
+ Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
1210
+ Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devMongokit);
1211
+ }
1104
1212
  if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
1213
+ applyLatestOverlay(dependencies);
1214
+ applyLatestOverlay(devDependencies);
1105
1215
  overlayLiveArcVersions(dependencies);
1106
1216
  return {
1107
1217
  dependencies: sortByKey(dependencies),
@@ -3030,6 +3140,7 @@ async function createProjectStructure(projectPath, config) {
3030
3140
  files["docker-compose.yml"] = dockerComposeTemplate(config);
3031
3141
  }
3032
3142
  if (config.edge) files["wrangler.toml"] = wranglerTemplate(config);
3143
+ files["CLAUDE.md"] = agentsGuideTemplate(config);
3033
3144
  files[".arcrc"] = `${JSON.stringify({
3034
3145
  adapter: config.adapter,
3035
3146
  auth: config.auth,
@@ -3329,6 +3440,14 @@ async function init(options = {}) {
3329
3440
  }
3330
3441
  const packageManager = detectPackageManager();
3331
3442
  console.log(`Using package manager: ${packageManager}\n`);
3443
+ if (!process.env.VITEST && process.env.ARC_INIT_OFFLINE !== "1") {
3444
+ const { primeLatestScaffoldVersions } = await Promise.resolve().then(() => dependency_plan_exports);
3445
+ const { resolved, total } = await primeLatestScaffoldVersions().catch(() => ({
3446
+ resolved: 0,
3447
+ total: 0
3448
+ }));
3449
+ console.log(resolved > 0 ? `Resolved ${resolved}/${total} dependency versions from npm (latest stable)\n` : "Registry unreachable — using pinned fallback versions\n");
3450
+ }
3332
3451
  await createProjectStructure(projectPath, config);
3333
3452
  if (!options.skipInstall) {
3334
3453
  console.log("\n📥 Installing dependencies...\n");
@@ -4,7 +4,7 @@ const eventGatewayPluginImpl = async (fastify, opts = {}) => {
4
4
  const { auth = true, orgScoped = false, roomPolicy, maxMessageBytes, maxSubscriptionsPerClient, authenticate } = opts;
5
5
  if (auth && !authenticate && !fastify.hasDecorator("authenticate")) throw new Error("[arc-event-gateway] auth is true but fastify.authenticate is not registered. Register an auth plugin first, provide a custom authenticate function, or set auth: false.");
6
6
  if (opts.sse !== false) {
7
- const { default: ssePlugin } = await import("../sse-CfUjwjDk.mjs").then((n) => n.r);
7
+ const { default: ssePlugin } = await import("../sse-0u4P4-OI.mjs").then((n) => n.r);
8
8
  await fastify.register(ssePlugin, {
9
9
  path: opts.sse?.path ?? "/events/stream",
10
10
  requireAuth: auth,
@@ -1,7 +1,7 @@
1
1
  import { c as WebSocketClient, l as WebSocketMessage, u as WebSocketPluginOptions } from "../websocket-uuWqM6UY.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-DRj-Ut4n.mjs";
4
+ import { _ as ToolDefinition, c as McpAuthResult, d as McpResourceConfig, f as PromptDefinition, g as ToolContext, h as ToolAnnotations, n as CallToolResult, o as CrudOperation, r as CreateMcpServerConfig, t as BetterAuthHandler, u as McpPluginOptions } from "../types-5OuqeTDX.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,8 @@
1
- import { V as ResourceDefinition, bt as IControllerResponse, xt as IRequestContext } from "../../index-Dtrj6XZk.mjs";
1
+ import { Rt as IControllerResponse, V as ResourceDefinition, Vt as ServerAccessor, zt as IRequestContext } from "../../index-BBsvQt7U.mjs";
2
2
  import { i as RequestScope } from "../../types-DH0GupI7.mjs";
3
- import { c as PermissionCheck } from "../../fields-BGqrAa_u.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-DRj-Ut4n.mjs";
3
+ import { t as PermissionCheck } from "../../types-Csxga7d3.mjs";
4
+ import { n as FieldPermissionMap } from "../../fields-D3lhgzhD.mjs";
5
+ import { _ as ToolDefinition, a as CrudDescriptionOverride, c as McpAuthResult, d as McpResourceConfig, f as PromptDefinition, g as ToolContext, h as ToolAnnotations, i as CrudDescriptionMeta, l as McpExecutionWiring, m as SessionEntry, n as CallToolResult, o as CrudOperation, p as PromptResult, r as CreateMcpServerConfig, s as McpAuthResolver, t as BetterAuthHandler, u as McpPluginOptions } from "../../types-5OuqeTDX.mjs";
5
6
  import { ErrorContract } from "@classytic/repo-core/errors";
6
7
  import { FastifyPluginAsync } from "fastify";
7
8
  import { z } from "zod";
@@ -299,6 +300,34 @@ declare function customGuard(check: (ctx: ToolContext) => boolean | Promise<bool
299
300
  //#endregion
300
301
  //#region src/integrations/mcp/buildRequestContext.d.ts
301
302
  type McpOperation = "list" | "get" | "create" | "update" | "delete" | "action";
303
+ /**
304
+ * Optional context extras — the pieces of `IRequestContext` that HTTP's
305
+ * `createRequestContext` assembles from Fastify decorators and the MCP
306
+ * synthetic path historically DROPPED. Without them, controller code that
307
+ * reads `metadata.arc.hooks` (the whole hook system — including the
308
+ * arcCorePlugin after-hook that publishes `<resource>.<op>d` events) or
309
+ * `ctx.server.events` silently no-ops on MCP calls: agents could mutate
310
+ * records without hooks firing, events publishing, realtime feeds updating,
311
+ * or webhooks dispatching. Thread these through `invokeController` /
312
+ * `resourceToTools({ wiring })` to restore full HTTP ↔ MCP parity.
313
+ */
314
+ interface McpContextExtras {
315
+ /**
316
+ * Resource wiring — becomes `metadata.arc`, the same frozen shape
317
+ * `buildArcDecorator` stamps on HTTP requests (`ArcRouteMeta`).
318
+ */
319
+ arc?: {
320
+ readonly resourceName: string;
321
+ readonly schemaOptions?: unknown;
322
+ readonly permissions?: unknown;
323
+ readonly hooks?: unknown;
324
+ readonly events?: unknown;
325
+ readonly fields?: unknown;
326
+ readonly idField?: string;
327
+ };
328
+ /** Server accessor — `ctx.server.events/audit/log`, mirroring HTTP. */
329
+ server?: ServerAccessor;
330
+ }
302
331
  /**
303
332
  * Build an IRequestContext from MCP tool input and session auth.
304
333
  *
@@ -317,7 +346,7 @@ type McpOperation = "list" | "get" | "create" | "update" | "delete" | "action";
317
346
  * the session-derived scope is `public` (i.e. MCP called with `auth: false`).
318
347
  * An authenticated session scope is never overwritten.
319
348
  */
320
- declare function buildRequestContext(input: Record<string, unknown>, auth: McpAuthResult | null, operation: McpOperation, policyFilters?: Record<string, unknown>, scopeOverride?: RequestScope): IRequestContext;
349
+ declare function buildRequestContext(input: Record<string, unknown>, auth: McpAuthResult | null, operation: McpOperation, policyFilters?: Record<string, unknown>, scopeOverride?: RequestScope, extras?: McpContextExtras): IRequestContext;
321
350
  //#endregion
322
351
  //#region src/integrations/mcp/invokeController.d.ts
323
352
  /**
@@ -349,6 +378,33 @@ interface InvokeControllerOptions {
349
378
  * action handler reached through a `string` route handler).
350
379
  */
351
380
  readonly methodName?: string;
381
+ /**
382
+ * Resource field-permission map (`ResourceConfig.fields`). When present,
383
+ * field-level READ permissions are applied to the response payload before
384
+ * serialization — the same masking the HTTP wire and realtime frames get.
385
+ * Omit only for tools whose output carries no resource documents.
386
+ */
387
+ readonly fields?: FieldPermissionMap;
388
+ /**
389
+ * App-level execution wiring (hooks system, event bus, audit, logger,
390
+ * idempotency store). When present, the synthetic request context carries
391
+ * `metadata.arc` + `server` exactly like HTTP's `createRequestContext` —
392
+ * so resource hooks run, CRUD events publish, and `ctx.server.*` works.
393
+ * `mcpPlugin` supplies this automatically; omit for dispatch-only calls.
394
+ */
395
+ readonly wiring?: McpExecutionWiring;
396
+ /** Resource `schemaOptions` — rides on `metadata.arc` like HTTP. */
397
+ readonly schemaOptions?: unknown;
398
+ /** Resource `idField` — rides on `metadata.arc` for `getEntityIdField()`. */
399
+ readonly idField?: string;
400
+ /**
401
+ * Caller-supplied idempotency key (mutations only). Requires
402
+ * `wiring.idempotencyStore`; the executor then runs the same
403
+ * check → lock → execute → record → replay protocol the HTTP
404
+ * idempotency plugin runs, keyed by tool + input + caller identity.
405
+ * A concurrent call with the same key gets a 409 `arc.idempotency_conflict`.
406
+ */
407
+ readonly idempotencyKey?: string;
352
408
  }
353
409
  /**
354
410
  * Invoke a controller method through the MCP synthetic-context path.
@@ -549,6 +605,13 @@ interface ResourceToToolsConfig extends McpResourceConfig {
549
605
  toolNamePrefix?: string;
550
606
  /** Per-operation tool name overrides: `{ get: 'get_job_by_id' }` */
551
607
  names?: Partial<Record<CrudOperation, string>>;
608
+ /**
609
+ * App-level execution wiring (hooks, events, audit, log, idempotency
610
+ * store). `mcpPlugin` supplies this automatically from its Fastify
611
+ * instance; standalone callers pass their own or omit it for
612
+ * dispatch-only tools (no hooks/events/idempotency).
613
+ */
614
+ wiring?: McpExecutionWiring;
552
615
  }
553
616
  /**
554
617
  * Convert a ResourceDefinition into MCP ToolDefinitions.
@@ -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-B9XjjNVG.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-BtzIL_kM.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-DRj-Ut4n.mjs";
1
+ import { c as McpAuthResult, u as McpPluginOptions } from "../../types-5OuqeTDX.mjs";
2
2
  //#region src/integrations/mcp/testing.d.ts
3
3
  interface TestMcpClientOptions {
4
4
  /** MCP plugin options (resources, overrides, etc.) — same as mcpPlugin config */
@@ -1,4 +1,4 @@
1
- import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-B9XjjNVG.mjs";
1
+ import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-BtzIL_kM.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -36,13 +36,51 @@ interface WebhookPluginOptions {
36
36
  timeout?: number;
37
37
  /** Max delivery log entries kept in memory (default: 1000) */
38
38
  maxLogEntries?: number;
39
- /** Max concurrent deliveries per event (default: 5). Set to 1 for sequential. */
39
+ /**
40
+ * Max concurrent deliveries per event (default: 5). Set to 1 for
41
+ * sequential. Must be >= 1 — the plugin throws at registration otherwise
42
+ * (a zero would make the delivery loop spin forever without progress).
43
+ */
40
44
  concurrency?: number;
45
+ /**
46
+ * URL policy hook — the SSRF seam for tenant-registered URLs. Throw to
47
+ * reject. Pass {@link defaultWebhookUrlPolicy} to require HTTPS and block
48
+ * localhost / link-local / private-network / cloud-metadata destinations,
49
+ * or supply your own (allowlists, per-tenant rules).
50
+ *
51
+ * Enforcement points when set:
52
+ * - `register()` — new subscriptions are validated BEFORE saving.
53
+ * - Plugin init — every PERSISTED subscription is re-validated; failures
54
+ * are excluded from dispatch and logged (they stay in the store for
55
+ * the host to fix), so pre-policy rows and out-of-band store writes
56
+ * can't bypass the policy.
57
+ * - Delivery — `fetch` runs with `redirect: "manual"`, so an allowed
58
+ * public URL can't 302 into a blocked private/metadata target; a 3xx
59
+ * records as a failed delivery.
60
+ *
61
+ * Arc deliberately ships no DEFAULT policy — deployments differ (internal
62
+ * webhooks to private services are legitimate), and DNS-rebinding
63
+ * protection belongs at the actual outbound network boundary, not in the
64
+ * framework.
65
+ */
66
+ validateUrl?: (url: URL, subscription: WebhookSubscription) => void | Promise<void>;
41
67
  }
68
+ /** `WebhookSubscription` as returned by `list()` — secret withheld. */
69
+ type WebhookSubscriptionPublic = Omit<WebhookSubscription, "secret">;
42
70
  interface WebhookManager {
43
71
  register(sub: WebhookSubscription): Promise<void> | void;
44
72
  unregister(id: string): Promise<void> | void;
45
- list(): WebhookSubscription[];
73
+ /**
74
+ * Enumerate subscriptions WITHOUT their secrets — safe to surface in
75
+ * admin UIs / API responses. Use {@link WebhookManager.getSecret} for
76
+ * the privileged retrieval path.
77
+ */
78
+ list(): WebhookSubscriptionPublic[];
79
+ /**
80
+ * Privileged secret retrieval — separate from `list()` so enumerating
81
+ * subscriptions never leaks signing material into logs or UIs.
82
+ */
83
+ getSecret(id: string): string | undefined;
46
84
  deliveryLog(limit?: number): WebhookDeliveryRecord[];
47
85
  }
48
86
  declare module "fastify" {
@@ -53,9 +91,91 @@ declare module "fastify" {
53
91
  /**
54
92
  * Sign a payload with HMAC-SHA256 for outbound webhook delivery.
55
93
  *
94
+ * @deprecated LEGACY (pre-2.23) format — signs the BODY ONLY, so a captured
95
+ * request can be replayed indefinitely and the event-id header is not
96
+ * cryptographically bound to the body. Still emitted on the legacy
97
+ * `x-webhook-signature` header for existing receivers; new receivers should
98
+ * verify the v1 contract (`x-arc-webhook-signature`) via {@link verifyWebhook}.
99
+ * Legacy emission is scheduled for removal in v3.
100
+ *
56
101
  * @returns `sha256=<hex>` — the format written to `x-webhook-signature`
57
102
  */
58
103
  declare function signPayload(payload: string, secret: string): string;
104
+ /**
105
+ * Signed-string layout for the v1 webhook contract:
106
+ *
107
+ * v1.<timestampMs>.<deliveryId>.<body>
108
+ *
109
+ * Binding the timestamp bounds replay to the verifier's tolerance window,
110
+ * and binding the delivery id lets receivers deduplicate redeliveries on an
111
+ * AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
112
+ * attacker replaying a captured request could change it and defeat dedup).
113
+ *
114
+ * Wire headers:
115
+ * - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
116
+ * - `x-arc-webhook-timestamp: <unix epoch ms>`
117
+ * - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
118
+ * `meta.id` — STABLE across retries of the same event, so dedup on it
119
+ * collapses redeliveries)
120
+ */
121
+ declare function signWebhook(body: string, secret: string, meta: {
122
+ timestamp: number;
123
+ deliveryId: string;
124
+ }): string;
125
+ interface VerifyWebhookInput {
126
+ /** Raw request body — the exact bytes the sender signed (`req.rawBody`). */
127
+ body: string | Buffer;
128
+ /** Shared secret. */
129
+ secret: string;
130
+ /** `x-arc-webhook-signature` header value (`v1=<hex>`). */
131
+ signature: string | undefined;
132
+ /** `x-arc-webhook-timestamp` header value (unix epoch ms). */
133
+ timestamp: string | number | undefined;
134
+ /** `x-arc-webhook-delivery` header value. */
135
+ deliveryId: string | undefined;
136
+ /**
137
+ * Max allowed clock skew between sender timestamp and now, in ms.
138
+ * @default 300_000 (5 minutes)
139
+ */
140
+ toleranceMs?: number;
141
+ /** Injectable clock for tests. @default Date.now */
142
+ now?: () => number;
143
+ }
144
+ type VerifyWebhookResult = {
145
+ valid: true;
146
+ /** The AUTHENTICATED delivery id — dedup redeliveries on this. */
147
+ deliveryId: string;
148
+ /** Sender timestamp (unix epoch ms), authenticated. */
149
+ timestamp: number;
150
+ } | {
151
+ valid: false;
152
+ reason: "missing_headers" | "bad_timestamp" | "expired" | "bad_signature";
153
+ };
154
+ /**
155
+ * Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
156
+ * return the authenticated delivery id for receiver-side deduplication.
157
+ *
158
+ * ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
159
+ * never the parsed `req.body` — re-serialised JSON won't match the HMAC.
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
164
+ * const result = verifyWebhook({
165
+ * body: req.rawBody,
166
+ * secret: process.env.WEBHOOK_SECRET,
167
+ * signature: req.headers['x-arc-webhook-signature'] as string,
168
+ * timestamp: req.headers['x-arc-webhook-timestamp'] as string,
169
+ * deliveryId: req.headers['x-arc-webhook-delivery'] as string,
170
+ * });
171
+ * if (!result.valid) return reply.status(401).send({ error: result.reason });
172
+ * if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
173
+ * // ... handle, then mark result.deliveryId seen
174
+ * });
175
+ * ```
176
+ */
177
+ declare function verifyWebhook(input: VerifyWebhookInput): VerifyWebhookResult;
178
+ declare function defaultWebhookUrlPolicy(url: URL): void;
59
179
  /** Options for `verifySignature` — customize for non-Arc webhook senders */
60
180
  interface VerifySignatureOptions {
61
181
  /**
@@ -77,8 +197,9 @@ interface VerifySignatureOptions {
77
197
  *
78
198
  * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
79
199
  * parses JSON bodies by default and the re-serialised object will NOT match
80
- * the original signature. Register `@fastify/raw-body` or use `preParsing`
81
- * to capture `req.rawBody`, then pass that here.
200
+ * the original signature. Register `fastify-raw-body` (arc's createApp does
201
+ * this automatically with `global: false`) and opt the route in via
202
+ * `config: { rawBody: true }`, then pass `req.rawBody` here.
82
203
  *
83
204
  * @param body - Raw request body (string or Buffer — must be the exact bytes the sender signed)
84
205
  * @param secret - Shared secret between sender and receiver
@@ -90,8 +211,11 @@ interface VerifySignatureOptions {
90
211
  * ```typescript
91
212
  * import { verifySignature } from '@classytic/arc/integrations/webhooks';
92
213
  *
93
- * // Arc-to-Arc (default headers + format)
94
- * fastify.post('/webhooks/incoming', async (req, reply) => {
214
+ * // Arc-to-Arc (default headers + format). `config: { rawBody: true }` is
215
+ * // REQUIRED — arc registers fastify-raw-body with `global: false`, so
216
+ * // routes opt in per-route; without it `req.rawBody` is undefined and
217
+ * // verifySignature throws a TypeError.
218
+ * fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
95
219
  * const sig = req.headers['x-webhook-signature'] as string;
96
220
  * if (!verifySignature(req.rawBody, secret, sig)) {
97
221
  * return reply.status(401).send({ error: 'Invalid signature' });
@@ -114,4 +238,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
114
238
  declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
115
239
  declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
116
240
  //#endregion
117
- export { VerifySignatureOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, _default as default, signPayload, verifySignature, webhookPlugin };
241
+ export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };