@classytic/arc 2.22.0 → 2.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/dist/{BaseController-C6FlRWxF.mjs → BaseController--M8EMzt0.mjs} +92 -22
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  4. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  5. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  6. package/dist/audit/index.d.mts +32 -3
  7. package/dist/audit/index.mjs +7 -4
  8. package/dist/auth/index.d.mts +2 -2
  9. package/dist/auth/index.mjs +264 -260
  10. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  11. package/dist/cache/index.d.mts +41 -3
  12. package/dist/cache/index.mjs +33 -4
  13. package/dist/cli/commands/describe.d.mts +1 -1
  14. package/dist/cli/commands/describe.mjs +1 -1
  15. package/dist/cli/commands/docs.mjs +2 -2
  16. package/dist/cli/commands/generate.mjs +1 -1
  17. package/dist/cli/commands/init.mjs +1 -1
  18. package/dist/cli/commands/introspect.mjs +1 -1
  19. package/dist/cli/index.mjs +2 -2
  20. package/dist/context/index.mjs +2 -64
  21. package/dist/core/index.d.mts +3 -3
  22. package/dist/core/index.mjs +4 -4
  23. package/dist/{core-b6fgFgnd.mjs → core-EUNu0u07.mjs} +298 -12
  24. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-B6GvAuAX.mjs} +4 -3
  25. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  26. package/dist/docs/index.d.mts +1 -1
  27. package/dist/docs/index.mjs +2 -2
  28. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  29. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  30. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  31. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  32. package/dist/events/index.d.mts +57 -314
  33. package/dist/events/index.mjs +168 -93
  34. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  35. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  36. package/dist/factory/index.d.mts +21 -6
  37. package/dist/factory/index.mjs +58 -18
  38. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  39. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  40. package/dist/headers-BvlQSDMB.mjs +45 -0
  41. package/dist/health-CB_m4S32.mjs +377 -0
  42. package/dist/hooks/index.d.mts +1 -1
  43. package/dist/idempotency/index.d.mts +7 -0
  44. package/dist/idempotency/index.mjs +32 -20
  45. package/dist/{index-ogIHvOjo.d.mts → index-BMnbzvtO.d.mts} +70 -4
  46. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  47. package/dist/{index-DbtgCOCj.d.mts → index-DPRH0Cme.d.mts} +46 -94
  48. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  49. package/dist/{index-D8MKYGDa.d.mts → index-f4PTrk3B.d.mts} +37 -5
  50. package/dist/index.d.mts +6 -6
  51. package/dist/index.mjs +6 -5
  52. package/dist/{init-DleK3p9f.mjs → init-tlRSt667.mjs} +206 -205
  53. package/dist/integrations/event-gateway.d.mts +1 -1
  54. package/dist/integrations/event-gateway.mjs +1 -1
  55. package/dist/integrations/index.d.mts +3 -3
  56. package/dist/integrations/jobs/index.d.mts +2 -0
  57. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  58. package/dist/integrations/mcp/index.d.mts +68 -5
  59. package/dist/integrations/mcp/index.mjs +2 -2
  60. package/dist/integrations/mcp/testing.d.mts +1 -1
  61. package/dist/integrations/mcp/testing.mjs +1 -1
  62. package/dist/integrations/webhooks.d.mts +149 -22
  63. package/dist/integrations/webhooks.mjs +186 -42
  64. package/dist/integrations/websocket-redis.d.mts +1 -1
  65. package/dist/integrations/websocket.d.mts +1 -1
  66. package/dist/integrations/websocket.mjs +41 -14
  67. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  68. package/dist/interface-n5hpKg1d.mjs +16 -0
  69. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  70. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-Cb7olw0j.mjs} +316 -45
  71. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  72. package/dist/middleware/index.d.mts +17 -1
  73. package/dist/middleware/index.mjs +1 -1
  74. package/dist/migrations/index.d.mts +94 -44
  75. package/dist/migrations/index.mjs +196 -44
  76. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  77. package/dist/{openapi-BRpMZD7C.mjs → openapi-DrLtog_B.mjs} +4 -4
  78. package/dist/permissions/index.d.mts +3 -3
  79. package/dist/permissions/index.mjs +4 -3
  80. package/dist/{permissions-BJMuKMl7.mjs → permissions-BrtiBKGs.mjs} +305 -492
  81. package/dist/pipeline/index.d.mts +1 -1
  82. package/dist/{actionPermissions-ze70xXJi.mjs → pipeline-BN5G05rd.mjs} +4 -27
  83. package/dist/plugins/index.d.mts +7 -78
  84. package/dist/plugins/index.mjs +14 -97
  85. package/dist/plugins/response-cache.mjs +1 -0
  86. package/dist/plugins/tracing-entry.mjs +1 -1
  87. package/dist/{index-BEu4wjbL.d.mts → preloadResources-0oe-JSSS.d.mts} +59 -50
  88. package/dist/{createApp-Ci7yG5gv.mjs → preloadResources-DfyngIN9.mjs} +240 -68
  89. package/dist/presets/filesUpload.d.mts +3 -3
  90. package/dist/presets/filesUpload.mjs +3 -3
  91. package/dist/presets/index.d.mts +2 -1
  92. package/dist/presets/index.mjs +1 -1
  93. package/dist/presets/multiTenant.d.mts +1 -1
  94. package/dist/presets/multiTenant.mjs +1 -1
  95. package/dist/presets/search.d.mts +2 -2
  96. package/dist/presets/search.mjs +1 -1
  97. package/dist/{presets-gw0ciIUD.mjs → presets-DwFDVKP0.mjs} +2 -2
  98. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  99. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  100. package/dist/{registerRoute-xHc42fwB.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  101. package/dist/realtime-Cuw_deA-.mjs +375 -0
  102. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  103. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  104. package/dist/registry/index.d.mts +2 -2
  105. package/dist/registry/index.mjs +1 -1
  106. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  107. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  108. package/dist/schedules-CjOd2MG9.mjs +113 -0
  109. package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  110. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  111. package/dist/schemas/index.d.mts +67 -67
  112. package/dist/schemas/index.mjs +2 -3
  113. package/dist/scim/index.mjs +1 -2
  114. package/dist/scope/index.d.mts +2 -2
  115. package/dist/scope/index.mjs +4 -4
  116. package/dist/scope-DfaPntpo.mjs +36 -0
  117. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  118. package/dist/{sse-CfUjwjDk.mjs → sse-BQ1w0dfl.mjs} +8 -31
  119. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  120. package/dist/testing/index.d.mts +4 -3
  121. package/dist/testing/index.mjs +2 -2
  122. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  123. package/dist/types/index.d.mts +4 -4
  124. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  125. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  126. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  127. package/dist/{types-Bc42CUyk.d.mts → types-QFGc3Cjv.d.mts} +37 -2
  128. package/dist/usage/index.d.mts +3 -52
  129. package/dist/usage/index.mjs +0 -0
  130. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  131. package/dist/utils/index.d.mts +90 -57
  132. package/dist/utils/index.mjs +7 -3
  133. package/dist/{utils-FhJ88TJE.mjs → utils-taUVD4p3.mjs} +47 -385
  134. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  135. package/dist/{versioning-DLwklPlG.d.mts → versioning-DXgY5BUm.d.mts} +52 -31
  136. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  137. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  138. package/package.json +526 -453
  139. package/skills/arc/references/api-reference.md +3 -1
  140. package/skills/arc/references/integrations.md +20 -11
  141. package/skills/arc/references/production.md +734 -696
  142. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  143. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  144. package/dist/health-ScyjIVo5.mjs +0 -202
  145. package/dist/requestId-5BhTVzBv.mjs +0 -113
  146. package/dist/scope-k15RXfcq.mjs +0 -64
  147. package/dist/types-XTApa8b6.mjs +0 -24
@@ -1,45 +1,53 @@
1
1
  import fp from "fastify-plugin";
2
- //#region src/integrations/jobs.ts
3
- /**
4
- * Define a background job with typed data and configuration.
5
- *
6
- * @example
7
- * const processImage = defineJob({
8
- * name: 'process-image',
9
- * handler: async (data: { url: string; width: number }) => {
10
- * return await sharp(data.url).resize(data.width).toBuffer();
11
- * },
12
- * retries: 3,
13
- * timeout: 60000,
14
- * });
15
- */
16
- function defineJob(definition) {
17
- return definition;
18
- }
19
- var Semaphore = class {
20
- available;
21
- waiters = [];
22
- constructor(permits) {
23
- this.available = permits;
24
- }
25
- get isFull() {
26
- return this.available === 0;
27
- }
28
- acquire() {
29
- if (this.available > 0) {
30
- this.available--;
31
- return Promise.resolve();
32
- }
33
- return new Promise((resolve) => {
34
- this.waiters.push(resolve);
35
- });
36
- }
37
- release() {
38
- const next = this.waiters.shift();
39
- if (next) next();
40
- else this.available++;
2
+ //#region src/integrations/jobs/execution.ts
3
+ async function executeTimedHandler(options) {
4
+ const { run, label, jobId, timeoutMs, cancelGraceMs = 3e4, logger } = options;
5
+ let released = false;
6
+ const releaseSlot = () => {
7
+ if (released) return;
8
+ released = true;
9
+ options.releaseSlot?.();
10
+ };
11
+ const abort = new AbortController();
12
+ const handlerPromise = (async () => run(abort.signal))();
13
+ handlerPromise.then(releaseSlot, releaseSlot);
14
+ if (!timeoutMs) return handlerPromise;
15
+ let timer;
16
+ const timeoutPromise = new Promise((_, reject) => {
17
+ timer = setTimeout(() => {
18
+ const err = /* @__PURE__ */ new Error(`Job '${label}' timed out after ${timeoutMs}ms`);
19
+ abort.abort(err);
20
+ const timedOutAt = Date.now();
21
+ const graceTimer = setTimeout(() => {
22
+ logger?.warn({
23
+ job: label,
24
+ jobId,
25
+ graceMs: cancelGraceMs
26
+ }, "timed-out job handler still running after grace period — force-releasing its maxConcurrent slot (bulkhead may now be exceeded); make the handler honor its AbortSignal");
27
+ releaseSlot();
28
+ }, cancelGraceMs);
29
+ graceTimer.unref?.();
30
+ handlerPromise.then(() => logger?.warn({
31
+ job: label,
32
+ jobId,
33
+ afterMs: Date.now() - timedOutAt
34
+ }, "timed-out job handler eventually completed (result discarded)"), (handlerErr) => logger?.warn({
35
+ job: label,
36
+ jobId,
37
+ afterMs: Date.now() - timedOutAt,
38
+ err: handlerErr
39
+ }, "timed-out job handler eventually failed")).finally(() => clearTimeout(graceTimer));
40
+ reject(err);
41
+ }, timeoutMs);
42
+ });
43
+ try {
44
+ return await Promise.race([handlerPromise, timeoutPromise]);
45
+ } finally {
46
+ clearTimeout(timer);
41
47
  }
42
- };
48
+ }
49
+ //#endregion
50
+ //#region src/integrations/jobs/repeat.ts
43
51
  /**
44
52
  * Whether an existing schedule matches the currently configured repeat spec.
45
53
  *
@@ -60,11 +68,6 @@ function repeatSpecMatches(record, spec) {
60
68
  * Remove repeatable schedules left behind by a previous deploy whose repeat
61
69
  * spec no longer matches the current `JobDefinition.repeat`.
62
70
  *
63
- * BullMQ keys repeatable schedules by a hash of their repeat options, so
64
- * re-adding a job with a CHANGED pattern/every/tz/endDate creates a NEW
65
- * schedule while the old one keeps firing — the job double-fires after every
66
- * repeat-config change. This reconciles at plugin registration:
67
- *
68
71
  * - Only schedulers whose `name` equals the registered job's name are
69
72
  * candidates — foreign/unknown schedulers are never touched.
70
73
  * - A scheduler matching the current spec is kept (the subsequent
@@ -102,6 +105,32 @@ async function removeStaleRepeatSchedulers(queue, jobName, spec, log) {
102
105
  }, `[arc/jobs] Failed to reconcile repeatable schedules for '${jobName}' — continuing (boot-time hygiene only)`);
103
106
  }
104
107
  }
108
+ //#endregion
109
+ //#region src/integrations/jobs/plugin.ts
110
+ var Semaphore = class {
111
+ available;
112
+ waiters = [];
113
+ constructor(permits) {
114
+ this.available = permits;
115
+ }
116
+ get isFull() {
117
+ return this.available === 0;
118
+ }
119
+ acquire() {
120
+ if (this.available > 0) {
121
+ this.available--;
122
+ return Promise.resolve();
123
+ }
124
+ return new Promise((resolve) => {
125
+ this.waiters.push(resolve);
126
+ });
127
+ }
128
+ release() {
129
+ const next = this.waiters.shift();
130
+ if (next) next();
131
+ else this.available++;
132
+ }
133
+ };
105
134
  const jobsPluginImpl = async (fastify, options) => {
106
135
  const { connection, jobs, prefix = "/jobs", bridgeEvents = true, defaults = {} } = options;
107
136
  let Queue;
@@ -158,11 +187,6 @@ const jobsPluginImpl = async (fastify, options) => {
158
187
  const throttledSet = /* @__PURE__ */ new Set();
159
188
  throttledJobs.set(queueName, throttledSet);
160
189
  const worker = new Worker(queueName, async (bullJob) => {
161
- const meta = {
162
- jobId: bullJob.id ?? "",
163
- attemptsMade: bullJob.attemptsMade ?? 0,
164
- timestamp: Date.now()
165
- };
166
190
  if (semaphore) {
167
191
  if (semaphore.isFull) throttledSet.add(bullJob.id ?? "");
168
192
  try {
@@ -171,22 +195,23 @@ const jobsPluginImpl = async (fastify, options) => {
171
195
  throttledSet.delete(bullJob.id ?? "");
172
196
  }
173
197
  }
174
- let result;
175
- try {
176
- if (jobTimeout) {
177
- let timer;
178
- const timeoutPromise = new Promise((_, reject) => {
179
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Job '${queueName}' timed out after ${jobTimeout}ms`)), jobTimeout);
180
- });
181
- try {
182
- result = await Promise.race([job.handler(bullJob.data, meta), timeoutPromise]);
183
- } finally {
184
- clearTimeout(timer);
185
- }
186
- } else result = await job.handler(bullJob.data, meta);
187
- } finally {
188
- semaphore?.release();
189
- }
198
+ const result = await executeTimedHandler({
199
+ label: queueName,
200
+ ...bullJob.id !== void 0 ? { jobId: bullJob.id } : {},
201
+ ...jobTimeout !== void 0 ? { timeoutMs: jobTimeout } : {},
202
+ ...job.cancelGraceMs !== void 0 ? { cancelGraceMs: job.cancelGraceMs } : {},
203
+ ...semaphore ? { releaseSlot: () => semaphore.release() } : {},
204
+ logger: fastify.log,
205
+ run: (signal) => {
206
+ const meta = {
207
+ jobId: bullJob.id ?? "",
208
+ attemptsMade: bullJob.attemptsMade ?? 0,
209
+ timestamp: Date.now(),
210
+ signal
211
+ };
212
+ return job.handler(bullJob.data, meta);
213
+ }
214
+ });
190
215
  if (bridgeEvents && fastify.events?.publish) try {
191
216
  await fastify.events.publish(`job.${queueName}.completed`, {
192
217
  jobId: bullJob.id,
@@ -366,4 +391,22 @@ const jobsPlugin = fp(jobsPluginImpl, {
366
391
  fastify: "5.x"
367
392
  });
368
393
  //#endregion
369
- export { defineJob, jobsPlugin };
394
+ //#region src/integrations/jobs/types.ts
395
+ /**
396
+ * Define a background job with typed data and configuration.
397
+ *
398
+ * @example
399
+ * const processImage = defineJob({
400
+ * name: 'process-image',
401
+ * handler: async (data: { url: string; width: number }) => {
402
+ * return await sharp(data.url).resize(data.width).toBuffer();
403
+ * },
404
+ * retries: 3,
405
+ * timeout: 60000,
406
+ * });
407
+ */
408
+ function defineJob(definition) {
409
+ return definition;
410
+ }
411
+ //#endregion
412
+ export { defineJob, executeTimedHandler, jobsPlugin };
@@ -1,7 +1,8 @@
1
- import { Rt as IControllerResponse, V as ResourceDefinition, zt as IRequestContext } from "../../index-DbtgCOCj.mjs";
2
- import { i as RequestScope } from "../../types-DH0GupI7.mjs";
3
- import { t as PermissionCheck } from "../../types-Csxga7d3.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-Bc42CUyk.mjs";
1
+ import { Rt as IControllerResponse, V as ResourceDefinition, Vt as ServerAccessor, zt as IRequestContext } from "../../index-DPRH0Cme.mjs";
2
+ import { i as RequestScope } from "../../types-DjC-KUCz.mjs";
3
+ import { t as PermissionCheck } from "../../types-Dmw-6Qeo.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-QFGc3Cjv.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-ByA2tf6D.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-Cb7olw0j.mjs";
2
2
  //#region src/integrations/mcp/defineTool.ts
3
3
  /**
4
4
  * Define a type-safe MCP tool.
@@ -206,7 +206,7 @@ function createMcpAuthFromBetterAuthApiKey(auth, options = {}) {
206
206
  ...userId ? { userId } : {},
207
207
  ...organizationId ? { organizationId } : {},
208
208
  ...clientId ? { clientId } : {},
209
- ...scopes && scopes.length ? { scopes } : {}
209
+ ...scopes?.length ? { scopes } : {}
210
210
  };
211
211
  };
212
212
  }
@@ -1,4 +1,4 @@
1
- import { c as McpAuthResult, l as McpPluginOptions } from "../../types-Bc42CUyk.mjs";
1
+ import { c as McpAuthResult, u as McpPluginOptions } from "../../types-QFGc3Cjv.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-ByA2tf6D.mjs";
1
+ import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-Cb7olw0j.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -19,6 +19,8 @@ interface WebhookDeliveryRecord {
19
19
  status?: number;
20
20
  error?: string;
21
21
  timestamp: Date;
22
+ /** Delivery attempts made (present when `retry` is configured). */
23
+ attempts?: number;
22
24
  }
23
25
  /** Pluggable persistence — memory for dev, bring your own DB for prod */
24
26
  interface WebhookStore {
@@ -36,13 +38,70 @@ interface WebhookPluginOptions {
36
38
  timeout?: number;
37
39
  /** Max delivery log entries kept in memory (default: 1000) */
38
40
  maxLogEntries?: number;
39
- /** Max concurrent deliveries per event (default: 5). Set to 1 for sequential. */
41
+ /**
42
+ * Max concurrent deliveries per event (default: 5). Set to 1 for
43
+ * sequential. Must be >= 1 — the plugin throws at registration otherwise
44
+ * (a zero would make the delivery loop spin forever without progress).
45
+ */
40
46
  concurrency?: number;
47
+ /**
48
+ * In-process retry for failed deliveries. Default: single attempt.
49
+ * Retries fire on network errors, timeouts, 429 and 5xx — NOT on other
50
+ * 4xx (the receiver rejected the payload; repeating it can't succeed).
51
+ * Each attempt is re-signed with a fresh timestamp so the receiver's
52
+ * replay-tolerance window applies per attempt; `x-arc-webhook-delivery`
53
+ * stays constant, which is exactly what receiver-side dedup keys on.
54
+ *
55
+ * This is BEST-EFFORT: attempts live in this process, so a crash or
56
+ * deploy drops the remaining ones. Guaranteed delivery is a durable-job
57
+ * concern (queue keyed by subscription + delivery id) — pair the events
58
+ * outbox with a worker if you need it.
59
+ */
60
+ retry?: {
61
+ /** Total attempts including the first. Must be an integer >= 1. */
62
+ attempts: number;
63
+ /** Backoff before retry N, as `backoffMs * 2^(N-1)` ms (default: 1000). */
64
+ backoffMs?: number;
65
+ };
66
+ /**
67
+ * URL policy hook — the SSRF seam for tenant-registered URLs. Throw to
68
+ * reject. Pass {@link defaultWebhookUrlPolicy} to require HTTPS and block
69
+ * localhost / link-local / private-network / cloud-metadata destinations,
70
+ * or supply your own (allowlists, per-tenant rules).
71
+ *
72
+ * Enforcement points when set:
73
+ * - `register()` — new subscriptions are validated BEFORE saving.
74
+ * - Plugin init — every PERSISTED subscription is re-validated; failures
75
+ * are excluded from dispatch and logged (they stay in the store for
76
+ * the host to fix), so pre-policy rows and out-of-band store writes
77
+ * can't bypass the policy.
78
+ * - Delivery — `fetch` runs with `redirect: "manual"`, so an allowed
79
+ * public URL can't 302 into a blocked private/metadata target; a 3xx
80
+ * records as a failed delivery.
81
+ *
82
+ * Arc deliberately ships no DEFAULT policy — deployments differ (internal
83
+ * webhooks to private services are legitimate), and DNS-rebinding
84
+ * protection belongs at the actual outbound network boundary, not in the
85
+ * framework.
86
+ */
87
+ validateUrl?: (url: URL, subscription: WebhookSubscription) => void | Promise<void>;
41
88
  }
89
+ /** `WebhookSubscription` as returned by `list()` — secret withheld. */
90
+ type WebhookSubscriptionPublic = Omit<WebhookSubscription, "secret">;
42
91
  interface WebhookManager {
43
92
  register(sub: WebhookSubscription): Promise<void> | void;
44
93
  unregister(id: string): Promise<void> | void;
45
- list(): WebhookSubscription[];
94
+ /**
95
+ * Enumerate subscriptions WITHOUT their secrets — safe to surface in
96
+ * admin UIs / API responses. Use {@link WebhookManager.getSecret} for
97
+ * the privileged retrieval path.
98
+ */
99
+ list(): WebhookSubscriptionPublic[];
100
+ /**
101
+ * Privileged secret retrieval — separate from `list()` so enumerating
102
+ * subscriptions never leaks signing material into logs or UIs.
103
+ */
104
+ getSecret(id: string): string | undefined;
46
105
  deliveryLog(limit?: number): WebhookDeliveryRecord[];
47
106
  }
48
107
  declare module "fastify" {
@@ -51,11 +110,80 @@ declare module "fastify" {
51
110
  }
52
111
  }
53
112
  /**
54
- * Sign a payload with HMAC-SHA256 for outbound webhook delivery.
113
+ * Signed-string layout for the v1 webhook contract:
114
+ *
115
+ * v1.<timestampMs>.<deliveryId>.<body>
55
116
  *
56
- * @returns `sha256=<hex>` — the format written to `x-webhook-signature`
117
+ * Binding the timestamp bounds replay to the verifier's tolerance window,
118
+ * and binding the delivery id lets receivers deduplicate redeliveries on an
119
+ * AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
120
+ * attacker replaying a captured request could change it and defeat dedup).
121
+ *
122
+ * Wire headers:
123
+ * - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
124
+ * - `x-arc-webhook-timestamp: <unix epoch ms>`
125
+ * - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
126
+ * `meta.id` — STABLE across retries of the same event, so dedup on it
127
+ * collapses redeliveries)
57
128
  */
58
- declare function signPayload(payload: string, secret: string): string;
129
+ declare function signWebhook(body: string, secret: string, meta: {
130
+ timestamp: number;
131
+ deliveryId: string;
132
+ }): string;
133
+ interface VerifyWebhookInput {
134
+ /** Raw request body — the exact bytes the sender signed (`req.rawBody`). */
135
+ body: string | Buffer;
136
+ /** Shared secret. */
137
+ secret: string;
138
+ /** `x-arc-webhook-signature` header value (`v1=<hex>`). */
139
+ signature: string | undefined;
140
+ /** `x-arc-webhook-timestamp` header value (unix epoch ms). */
141
+ timestamp: string | number | undefined;
142
+ /** `x-arc-webhook-delivery` header value. */
143
+ deliveryId: string | undefined;
144
+ /**
145
+ * Max allowed clock skew between sender timestamp and now, in ms.
146
+ * @default 300_000 (5 minutes)
147
+ */
148
+ toleranceMs?: number;
149
+ /** Injectable clock for tests. @default Date.now */
150
+ now?: () => number;
151
+ }
152
+ type VerifyWebhookResult = {
153
+ valid: true;
154
+ /** The AUTHENTICATED delivery id — dedup redeliveries on this. */
155
+ deliveryId: string;
156
+ /** Sender timestamp (unix epoch ms), authenticated. */
157
+ timestamp: number;
158
+ } | {
159
+ valid: false;
160
+ reason: "missing_headers" | "bad_timestamp" | "expired" | "bad_signature";
161
+ };
162
+ /**
163
+ * Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
164
+ * return the authenticated delivery id for receiver-side deduplication.
165
+ *
166
+ * ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
167
+ * never the parsed `req.body` — re-serialised JSON won't match the HMAC.
168
+ *
169
+ * @example
170
+ * ```typescript
171
+ * fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
172
+ * const result = verifyWebhook({
173
+ * body: req.rawBody,
174
+ * secret: process.env.WEBHOOK_SECRET,
175
+ * signature: req.headers['x-arc-webhook-signature'] as string,
176
+ * timestamp: req.headers['x-arc-webhook-timestamp'] as string,
177
+ * deliveryId: req.headers['x-arc-webhook-delivery'] as string,
178
+ * });
179
+ * if (!result.valid) return reply.status(401).send({ error: result.reason });
180
+ * if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
181
+ * // ... handle, then mark result.deliveryId seen
182
+ * });
183
+ * ```
184
+ */
185
+ declare function verifyWebhook(input: VerifyWebhookInput): VerifyWebhookResult;
186
+ declare function defaultWebhookUrlPolicy(url: URL): void;
59
187
  /** Options for `verifySignature` — customize for non-Arc webhook senders */
60
188
  interface VerifySignatureOptions {
61
189
  /**
@@ -65,15 +193,18 @@ interface VerifySignatureOptions {
65
193
  prefix?: string;
66
194
  /**
67
195
  * HMAC algorithm (default: `'sha256'`).
68
- * Must match what the sender uses — Arc's `signPayload` always uses sha256.
196
+ * Must match what the sender uses.
69
197
  */
70
198
  algorithm?: string;
71
199
  }
72
200
  /**
73
- * Verify an inbound webhook signature using timing-safe comparison.
201
+ * Verify an INBOUND third-party webhook signature using timing-safe
202
+ * comparison — for webhooks arc RECEIVES (GitHub, Stripe, ...), not for
203
+ * arc-to-arc delivery. Arc's own outbound contract is v1
204
+ * (`x-arc-webhook-*`) — receivers of arc webhooks use {@link verifyWebhook}.
74
205
  *
75
- * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
76
- * but configurable for any HMAC scheme via options.
206
+ * Defaults to the common `sha256=<hex>` format; configurable for any HMAC
207
+ * scheme via options.
77
208
  *
78
209
  * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
79
210
  * parses JSON bodies by default and the re-serialised object will NOT match
@@ -91,21 +222,17 @@ interface VerifySignatureOptions {
91
222
  * ```typescript
92
223
  * import { verifySignature } from '@classytic/arc/integrations/webhooks';
93
224
  *
94
- * // Arc-to-Arc (default headers + format). `config: { rawBody: true }` is
95
- * // REQUIRED — arc registers fastify-raw-body with `global: false`, so
96
- * // routes opt in per-route; without it `req.rawBody` is undefined and
97
- * // verifySignature throws a TypeError.
98
- * fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
99
- * const sig = req.headers['x-webhook-signature'] as string;
225
+ * // `config: { rawBody: true }` is REQUIRED — arc registers
226
+ * // fastify-raw-body with `global: false`, so routes opt in per-route;
227
+ * // without it `req.rawBody` is undefined and verifySignature throws a
228
+ * // TypeError.
229
+ *
230
+ * // GitHub-style sender (sha256=<hex>)
231
+ * fastify.post('/webhooks/github', { config: { rawBody: true } }, async (req, reply) => {
232
+ * const sig = req.headers['x-hub-signature-256'] as string;
100
233
  * if (!verifySignature(req.rawBody, secret, sig)) {
101
234
  * return reply.status(401).send({ error: 'Invalid signature' });
102
235
  * }
103
- * // handle event via req.headers['x-webhook-event']
104
- * });
105
- *
106
- * // Third-party sender with custom header + bare hex
107
- * const valid = verifySignature(body, secret, req.headers['x-hub-signature'], {
108
- * prefix: 'sha256=', // GitHub format
109
236
  * });
110
237
  *
111
238
  * // Stripe-style (bare hex, different header)
@@ -118,4 +245,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
118
245
  declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
119
246
  declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
120
247
  //#endregion
121
- export { VerifySignatureOptions, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, _default as default, signPayload, verifySignature, webhookPlugin };
248
+ export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signWebhook, verifySignature, verifyWebhook, webhookPlugin };