@classytic/arc 2.23.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 (132) hide show
  1. package/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
  2. package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
  3. package/dist/actionPermissions-CAphcmUG.mjs +24 -0
  4. package/dist/audit/index.d.mts +32 -3
  5. package/dist/audit/index.mjs +7 -4
  6. package/dist/auth/index.d.mts +2 -2
  7. package/dist/auth/index.mjs +263 -259
  8. package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
  9. package/dist/cache/index.d.mts +41 -3
  10. package/dist/cache/index.mjs +33 -4
  11. package/dist/cli/commands/describe.d.mts +1 -1
  12. package/dist/cli/commands/docs.mjs +1 -1
  13. package/dist/cli/commands/generate.mjs +1 -1
  14. package/dist/cli/commands/init.mjs +1 -1
  15. package/dist/cli/index.mjs +2 -2
  16. package/dist/context/index.mjs +2 -64
  17. package/dist/core/index.d.mts +3 -3
  18. package/dist/core/index.mjs +4 -4
  19. package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
  20. package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
  21. package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
  22. package/dist/docs/index.d.mts +1 -1
  23. package/dist/docs/index.mjs +2 -2
  24. package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
  25. package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
  26. package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
  27. package/dist/events/index.d.mts +57 -314
  28. package/dist/events/index.mjs +163 -91
  29. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  30. package/dist/events/transports/redis-stream-entry.mjs +24 -2
  31. package/dist/factory/index.d.mts +4 -5
  32. package/dist/factory/index.mjs +28 -8
  33. package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
  34. package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
  35. package/dist/headers-BvlQSDMB.mjs +45 -0
  36. package/dist/health-CB_m4S32.mjs +377 -0
  37. package/dist/hooks/index.d.mts +1 -1
  38. package/dist/idempotency/index.mjs +27 -3
  39. package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
  40. package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
  41. package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
  42. package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
  43. package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
  44. package/dist/index.d.mts +6 -6
  45. package/dist/index.mjs +5 -5
  46. package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
  47. package/dist/integrations/event-gateway.d.mts +1 -1
  48. package/dist/integrations/event-gateway.mjs +1 -1
  49. package/dist/integrations/index.d.mts +3 -3
  50. package/dist/integrations/jobs/index.d.mts +2 -0
  51. package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
  52. package/dist/integrations/mcp/index.d.mts +4 -4
  53. package/dist/integrations/mcp/index.mjs +2 -2
  54. package/dist/integrations/mcp/testing.d.mts +1 -1
  55. package/dist/integrations/mcp/testing.mjs +1 -1
  56. package/dist/integrations/webhooks.d.mts +37 -30
  57. package/dist/integrations/webhooks.mjs +51 -62
  58. package/dist/integrations/websocket-redis.d.mts +1 -1
  59. package/dist/integrations/websocket.d.mts +1 -1
  60. package/dist/integrations/websocket.mjs +41 -14
  61. package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
  62. package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
  63. package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
  64. package/dist/middleware/index.d.mts +17 -1
  65. package/dist/middleware/index.mjs +1 -1
  66. package/dist/migrations/index.d.mts +94 -44
  67. package/dist/migrations/index.mjs +196 -44
  68. package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
  69. package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
  70. package/dist/permissions/index.d.mts +2 -2
  71. package/dist/permissions/index.mjs +2 -2
  72. package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
  73. package/dist/pipeline/index.d.mts +1 -1
  74. package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
  75. package/dist/plugins/index.d.mts +6 -77
  76. package/dist/plugins/index.mjs +11 -95
  77. package/dist/plugins/response-cache.mjs +1 -0
  78. package/dist/plugins/tracing-entry.mjs +1 -1
  79. package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
  80. package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
  81. package/dist/presets/filesUpload.d.mts +3 -3
  82. package/dist/presets/filesUpload.mjs +3 -3
  83. package/dist/presets/index.d.mts +2 -1
  84. package/dist/presets/index.mjs +1 -1
  85. package/dist/presets/multiTenant.d.mts +1 -1
  86. package/dist/presets/multiTenant.mjs +1 -1
  87. package/dist/presets/search.d.mts +2 -2
  88. package/dist/presets/search.mjs +1 -1
  89. package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
  90. package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
  91. package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
  92. package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
  93. package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
  94. package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
  95. package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
  96. package/dist/registry/index.d.mts +2 -2
  97. package/dist/renewingLease-3cuQH7h8.mjs +44 -0
  98. package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
  99. package/dist/schedules-CjOd2MG9.mjs +113 -0
  100. package/dist/schemas/index.d.mts +67 -67
  101. package/dist/schemas/index.mjs +2 -3
  102. package/dist/scim/index.mjs +1 -2
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +4 -4
  105. package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
  106. package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
  107. package/dist/testing/index.d.mts +4 -3
  108. package/dist/testing/index.mjs +2 -2
  109. package/dist/transactionContext-PNdoyLLm.mjs +64 -0
  110. package/dist/types/index.d.mts +4 -4
  111. package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
  112. package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
  113. package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
  114. package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
  115. package/dist/usage/index.d.mts +3 -52
  116. package/dist/usage/index.mjs +0 -0
  117. package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
  118. package/dist/utils/index.d.mts +58 -85
  119. package/dist/utils/index.mjs +4 -2
  120. package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
  121. package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
  122. package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
  123. package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
  124. package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
  125. package/package.json +526 -513
  126. package/skills/arc/references/api-reference.md +3 -1
  127. package/skills/arc/references/integrations.md +20 -11
  128. package/dist/QueryCache-Z1B38koh.d.mts +0 -40
  129. package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
  130. package/dist/health-ScyjIVo5.mjs +0 -202
  131. package/dist/requestId-5BhTVzBv.mjs +0 -113
  132. 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,8 +1,8 @@
1
- import { Rt as IControllerResponse, V as ResourceDefinition, Vt as ServerAccessor, zt as IRequestContext } from "../../index-BBsvQt7U.mjs";
2
- import { i as RequestScope } from "../../types-DH0GupI7.mjs";
3
- import { t as PermissionCheck } from "../../types-Csxga7d3.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
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
+ 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";
6
6
  import { ErrorContract } from "@classytic/repo-core/errors";
7
7
  import { FastifyPluginAsync } from "fastify";
8
8
  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-BtzIL_kM.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, u as McpPluginOptions } from "../../types-5OuqeTDX.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-BtzIL_kM.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 {
@@ -42,6 +44,25 @@ interface WebhookPluginOptions {
42
44
  * (a zero would make the delivery loop spin forever without progress).
43
45
  */
44
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
+ };
45
66
  /**
46
67
  * URL policy hook — the SSRF seam for tenant-registered URLs. Throw to
47
68
  * reject. Pass {@link defaultWebhookUrlPolicy} to require HTTPS and block
@@ -88,19 +109,6 @@ declare module "fastify" {
88
109
  webhooks: WebhookManager;
89
110
  }
90
111
  }
91
- /**
92
- * Sign a payload with HMAC-SHA256 for outbound webhook delivery.
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
- *
101
- * @returns `sha256=<hex>` — the format written to `x-webhook-signature`
102
- */
103
- declare function signPayload(payload: string, secret: string): string;
104
112
  /**
105
113
  * Signed-string layout for the v1 webhook contract:
106
114
  *
@@ -185,15 +193,18 @@ interface VerifySignatureOptions {
185
193
  prefix?: string;
186
194
  /**
187
195
  * HMAC algorithm (default: `'sha256'`).
188
- * Must match what the sender uses — Arc's `signPayload` always uses sha256.
196
+ * Must match what the sender uses.
189
197
  */
190
198
  algorithm?: string;
191
199
  }
192
200
  /**
193
- * 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}.
194
205
  *
195
- * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
196
- * but configurable for any HMAC scheme via options.
206
+ * Defaults to the common `sha256=<hex>` format; configurable for any HMAC
207
+ * scheme via options.
197
208
  *
198
209
  * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
199
210
  * parses JSON bodies by default and the re-serialised object will NOT match
@@ -211,21 +222,17 @@ interface VerifySignatureOptions {
211
222
  * ```typescript
212
223
  * import { verifySignature } from '@classytic/arc/integrations/webhooks';
213
224
  *
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) => {
219
- * 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;
220
233
  * if (!verifySignature(req.rawBody, secret, sig)) {
221
234
  * return reply.status(401).send({ error: 'Invalid signature' });
222
235
  * }
223
- * // handle event via req.headers['x-webhook-event']
224
- * });
225
- *
226
- * // Third-party sender with custom header + bare hex
227
- * const valid = verifySignature(body, secret, req.headers['x-hub-signature'], {
228
- * prefix: 'sha256=', // GitHub format
229
236
  * });
230
237
  *
231
238
  * // Stripe-style (bare hex, different header)
@@ -238,4 +245,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
238
245
  declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
239
246
  declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
240
247
  //#endregion
241
- export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
248
+ export { VerifySignatureOptions, VerifyWebhookInput, VerifyWebhookResult, WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription, WebhookSubscriptionPublic, _default as default, defaultWebhookUrlPolicy, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
@@ -31,23 +31,6 @@ import fp from "fastify-plugin";
31
31
  * ```
32
32
  */
33
33
  /**
34
- * Sign a payload with HMAC-SHA256 for outbound webhook delivery.
35
- *
36
- * @deprecated LEGACY (pre-2.23) format — signs the BODY ONLY, so a captured
37
- * request can be replayed indefinitely and the event-id header is not
38
- * cryptographically bound to the body. Still emitted on the legacy
39
- * `x-webhook-signature` header for existing receivers; new receivers should
40
- * verify the v1 contract (`x-arc-webhook-signature`) via {@link verifyWebhook}.
41
- * Legacy emission is scheduled for removal in v3.
42
- *
43
- * @returns `sha256=<hex>` — the format written to `x-webhook-signature`
44
- */
45
- function signPayload(payload, secret) {
46
- const hmac = createHmac("sha256", secret);
47
- hmac.update(payload);
48
- return `sha256=${hmac.digest("hex")}`;
49
- }
50
- /**
51
34
  * Signed-string layout for the v1 webhook contract:
52
35
  *
53
36
  * v1.<timestampMs>.<deliveryId>.<body>
@@ -162,10 +145,13 @@ function decodeMappedV4(host) {
162
145
  return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
163
146
  }
164
147
  /**
165
- * Verify an inbound webhook signature using timing-safe comparison.
148
+ * Verify an INBOUND third-party webhook signature using timing-safe
149
+ * comparison — for webhooks arc RECEIVES (GitHub, Stripe, ...), not for
150
+ * arc-to-arc delivery. Arc's own outbound contract is v1
151
+ * (`x-arc-webhook-*`) — receivers of arc webhooks use {@link verifyWebhook}.
166
152
  *
167
- * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
168
- * but configurable for any HMAC scheme via options.
153
+ * Defaults to the common `sha256=<hex>` format; configurable for any HMAC
154
+ * scheme via options.
169
155
  *
170
156
  * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
171
157
  * parses JSON bodies by default and the re-serialised object will NOT match
@@ -183,21 +169,17 @@ function decodeMappedV4(host) {
183
169
  * ```typescript
184
170
  * import { verifySignature } from '@classytic/arc/integrations/webhooks';
185
171
  *
186
- * // Arc-to-Arc (default headers + format). `config: { rawBody: true }` is
187
- * // REQUIRED — arc registers fastify-raw-body with `global: false`, so
188
- * // routes opt in per-route; without it `req.rawBody` is undefined and
189
- * // verifySignature throws a TypeError.
190
- * fastify.post('/webhooks/incoming', { config: { rawBody: true } }, async (req, reply) => {
191
- * const sig = req.headers['x-webhook-signature'] as string;
172
+ * // `config: { rawBody: true }` is REQUIRED — arc registers
173
+ * // fastify-raw-body with `global: false`, so routes opt in per-route;
174
+ * // without it `req.rawBody` is undefined and verifySignature throws a
175
+ * // TypeError.
176
+ *
177
+ * // GitHub-style sender (sha256=<hex>)
178
+ * fastify.post('/webhooks/github', { config: { rawBody: true } }, async (req, reply) => {
179
+ * const sig = req.headers['x-hub-signature-256'] as string;
192
180
  * if (!verifySignature(req.rawBody, secret, sig)) {
193
181
  * return reply.status(401).send({ error: 'Invalid signature' });
194
182
  * }
195
- * // handle event via req.headers['x-webhook-event']
196
- * });
197
- *
198
- * // Third-party sender with custom header + bare hex
199
- * const valid = verifySignature(body, secret, req.headers['x-hub-signature'], {
200
- * prefix: 'sha256=', // GitHub format
201
183
  * });
202
184
  *
203
185
  * // Stripe-style (bare hex, different header)
@@ -255,6 +237,9 @@ const webhookPlugin = async (fastify, opts = {}) => {
255
237
  const maxLogEntries = opts.maxLogEntries ?? 1e3;
256
238
  const concurrency = opts.concurrency ?? 5;
257
239
  if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`webhookPlugin: concurrency must be an integer >= 1 (got ${String(opts.concurrency)})`);
240
+ const retryAttempts = opts.retry?.attempts ?? 1;
241
+ const retryBackoffMs = opts.retry?.backoffMs ?? 1e3;
242
+ if (!Number.isInteger(retryAttempts) || retryAttempts < 1) throw new Error(`webhookPlugin: retry.attempts must be an integer >= 1 (got ${String(opts.retry?.attempts)})`);
258
243
  let subscriptions = [];
259
244
  const log = [];
260
245
  subscriptions = await store.getAll();
@@ -287,38 +272,42 @@ const webhookPlugin = async (fastify, opts = {}) => {
287
272
  success: false,
288
273
  timestamp: /* @__PURE__ */ new Date()
289
274
  };
290
- try {
291
- const timestamp = Date.now();
292
- const v1Signature = signWebhook(body, sub.secret, {
293
- timestamp,
294
- deliveryId: event.meta.id
295
- });
296
- const legacySignature = signPayload(body, sub.secret);
297
- const ac = new AbortController();
298
- const timer = setTimeout(() => ac.abort(), timeout);
275
+ for (let attempt = 1; attempt <= retryAttempts; attempt++) {
276
+ record.attempts = attempt;
277
+ delete record.status;
278
+ delete record.error;
299
279
  try {
300
- const response = await fetchFn(sub.url, {
301
- method: "POST",
302
- headers: {
303
- "content-type": "application/json",
304
- "x-arc-webhook-signature": v1Signature,
305
- "x-arc-webhook-timestamp": String(timestamp),
306
- "x-arc-webhook-delivery": event.meta.id,
307
- "x-webhook-signature": legacySignature,
308
- "x-webhook-id": event.meta.id,
309
- "x-webhook-event": event.type
310
- },
311
- body,
312
- signal: ac.signal,
313
- ...opts.validateUrl ? { redirect: "manual" } : {}
280
+ const timestamp = Date.now();
281
+ const v1Signature = signWebhook(body, sub.secret, {
282
+ timestamp,
283
+ deliveryId: event.meta.id
314
284
  });
315
- record.success = response.ok;
316
- record.status = response.status;
317
- } finally {
318
- clearTimeout(timer);
285
+ const ac = new AbortController();
286
+ const timer = setTimeout(() => ac.abort(), timeout);
287
+ try {
288
+ const response = await fetchFn(sub.url, {
289
+ method: "POST",
290
+ headers: {
291
+ "content-type": "application/json",
292
+ "x-arc-webhook-signature": v1Signature,
293
+ "x-arc-webhook-timestamp": String(timestamp),
294
+ "x-arc-webhook-delivery": event.meta.id
295
+ },
296
+ body,
297
+ signal: ac.signal,
298
+ ...opts.validateUrl ? { redirect: "manual" } : {}
299
+ });
300
+ record.success = response.ok;
301
+ record.status = response.status;
302
+ } finally {
303
+ clearTimeout(timer);
304
+ }
305
+ } catch (err) {
306
+ record.error = err instanceof Error ? err.message : String(err);
319
307
  }
320
- } catch (err) {
321
- record.error = err instanceof Error ? err.message : String(err);
308
+ if (record.success) break;
309
+ if (!(record.status === void 0 || record.status === 429 || record.status >= 500) || attempt === retryAttempts) break;
310
+ await new Promise((r) => setTimeout(r, retryBackoffMs * 2 ** (attempt - 1)));
322
311
  }
323
312
  log.push(record);
324
313
  if (log.length > maxLogEntries) log.splice(0, log.length - maxLogEntries);
@@ -372,4 +361,4 @@ var webhooks_default = fp(webhookPlugin, {
372
361
  dependencies: ["arc-events"]
373
362
  });
374
363
  //#endregion
375
- export { webhooks_default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
364
+ export { webhooks_default as default, defaultWebhookUrlPolicy, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
@@ -1,4 +1,4 @@
1
- import { f as WebSocketAdapter } from "../websocket-uuWqM6UY.mjs";
1
+ import { f as WebSocketAdapter } from "../websocket-_Pd0CgR0.mjs";
2
2
  //#region src/integrations/websocket-redis.d.ts
3
3
  interface RedisLike {
4
4
  publish(channel: string, message: string): Promise<number>;
@@ -1,3 +1,3 @@
1
- import { a as GeoRoomOptions, c as WebSocketClient, d as LocalWebSocketAdapter, f as WebSocketAdapter, i as GeoRoom, l as WebSocketMessage, n as GeoPoint, o as RoomManager, r as GeoPublishOpts, s as AuthResult, t as websocketPlugin, u as WebSocketPluginOptions } from "../websocket-uuWqM6UY.mjs";
1
+ import { a as GeoRoomOptions, c as WebSocketClient, d as LocalWebSocketAdapter, f as WebSocketAdapter, i as GeoRoom, l as WebSocketMessage, n as GeoPoint, o as RoomManager, r as GeoPublishOpts, s as AuthResult, t as websocketPlugin, u as WebSocketPluginOptions } from "../websocket-_Pd0CgR0.mjs";
2
2
  import { a as PushRefRegistry, c as SerializedEntry, i as Principal, o as PushRefRegistryOptions, r as MemoryPushRefStore, s as PushRefStore } from "../pushref-registry-BbcnpNTm.mjs";
3
3
  export { type AuthResult, type GeoPoint, type GeoPublishOpts, GeoRoom, type GeoRoomOptions, LocalWebSocketAdapter, MemoryPushRefStore, type Principal, PushRefRegistry, type PushRefRegistryOptions, type PushRefStore, RoomManager, type SerializedEntry, type WebSocketAdapter, type WebSocketClient, type WebSocketMessage, type WebSocketPluginOptions, websocketPlugin };