@classytic/arc 2.22.0 → 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/dist/{BaseController-C6FlRWxF.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
  2. package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
  3. package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
  4. package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
  5. package/dist/audit/index.d.mts +1 -1
  6. package/dist/auth/index.d.mts +1 -1
  7. package/dist/auth/index.mjs +2 -2
  8. package/dist/cli/commands/describe.d.mts +1 -1
  9. package/dist/cli/commands/describe.mjs +1 -1
  10. package/dist/cli/commands/docs.mjs +2 -2
  11. package/dist/cli/commands/init.mjs +1 -1
  12. package/dist/cli/commands/introspect.mjs +1 -1
  13. package/dist/cli/index.mjs +1 -1
  14. package/dist/core/index.d.mts +2 -2
  15. package/dist/core/index.mjs +3 -3
  16. package/dist/{core-b6fgFgnd.mjs → core-DQ6rDTyH.mjs} +10 -9
  17. package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-BuOv-m4Y.mjs} +3 -3
  18. package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-CgJv_BU_.mjs} +1 -1
  19. package/dist/{createApp-Ci7yG5gv.mjs → createApp-CjOeWJ9_.mjs} +11 -2
  20. package/dist/docs/index.d.mts +1 -1
  21. package/dist/docs/index.mjs +1 -1
  22. package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
  23. package/dist/events/index.mjs +5 -2
  24. package/dist/factory/index.d.mts +20 -4
  25. package/dist/factory/index.mjs +31 -11
  26. package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-g_f9omSc.d.mts} +1 -1
  27. package/dist/hooks/index.d.mts +1 -1
  28. package/dist/idempotency/index.d.mts +7 -0
  29. package/dist/idempotency/index.mjs +5 -17
  30. package/dist/{index-DbtgCOCj.d.mts → index-BBsvQt7U.d.mts} +26 -4
  31. package/dist/{index-BEu4wjbL.d.mts → index-Ba10fdKp.d.mts} +11 -2
  32. package/dist/{index-D8MKYGDa.d.mts → index-CC5ruDGh.d.mts} +2 -2
  33. package/dist/{index-ogIHvOjo.d.mts → index-wMoWBLkn.d.mts} +67 -1
  34. package/dist/index.d.mts +4 -4
  35. package/dist/index.mjs +6 -5
  36. package/dist/{init-DleK3p9f.mjs → init-CjO4p_q1.mjs} +5 -2
  37. package/dist/integrations/event-gateway.mjs +1 -1
  38. package/dist/integrations/index.d.mts +1 -1
  39. package/dist/integrations/mcp/index.d.mts +66 -3
  40. package/dist/integrations/mcp/index.mjs +1 -1
  41. package/dist/integrations/mcp/testing.d.mts +1 -1
  42. package/dist/integrations/mcp/testing.mjs +1 -1
  43. package/dist/integrations/webhooks.d.mts +123 -3
  44. package/dist/integrations/webhooks.mjs +160 -5
  45. package/dist/interface-n5hpKg1d.mjs +16 -0
  46. package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
  47. package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-BtzIL_kM.mjs} +308 -39
  48. package/dist/middleware/index.d.mts +1 -1
  49. package/dist/{openapi-BRpMZD7C.mjs → openapi-jSDx-VDv.mjs} +3 -3
  50. package/dist/permissions/index.d.mts +2 -2
  51. package/dist/permissions/index.mjs +3 -2
  52. package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
  53. package/dist/pipeline/index.d.mts +1 -1
  54. package/dist/plugins/index.d.mts +6 -6
  55. package/dist/plugins/index.mjs +5 -4
  56. package/dist/plugins/tracing-entry.mjs +1 -1
  57. package/dist/presets/filesUpload.d.mts +1 -1
  58. package/dist/presets/filesUpload.mjs +1 -1
  59. package/dist/presets/index.d.mts +1 -1
  60. package/dist/presets/index.mjs +1 -1
  61. package/dist/presets/multiTenant.d.mts +1 -1
  62. package/dist/presets/search.d.mts +1 -1
  63. package/dist/presets/search.mjs +1 -1
  64. package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
  65. package/dist/realtime-I8OrfnMR.mjs +375 -0
  66. package/dist/{registerRoute-xHc42fwB.mjs → registerRoute-CWO6mdYe.mjs} +1 -1
  67. package/dist/registry/index.d.mts +2 -2
  68. package/dist/registry/index.mjs +1 -1
  69. package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
  70. package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
  71. package/dist/scope/index.mjs +1 -1
  72. package/dist/scope-LONQ3mfZ.mjs +37 -0
  73. package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
  74. package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
  75. package/dist/sseStream-BQ7Qaynp.mjs +57 -0
  76. package/dist/testing/index.d.mts +2 -2
  77. package/dist/testing/index.mjs +2 -2
  78. package/dist/types/index.d.mts +1 -1
  79. package/dist/{types-Bc42CUyk.d.mts → types-5OuqeTDX.d.mts} +37 -2
  80. package/dist/utils/index.d.mts +62 -2
  81. package/dist/utils/index.mjs +5 -3
  82. package/dist/{utils-FhJ88TJE.mjs → utils-CZyeRXTh.mjs} +21 -69
  83. package/dist/{versioning-DLwklPlG.d.mts → versioning-D6PvCwpq.d.mts} +43 -4
  84. package/package.json +64 -4
  85. package/skills/arc/references/production.md +734 -696
  86. package/dist/scope-k15RXfcq.mjs +0 -64
@@ -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
  /**
@@ -118,4 +238,4 @@ declare function verifySignature(body: string | Buffer, secret: string, signatur
118
238
  declare const webhookPlugin: FastifyPluginAsync<WebhookPluginOptions>;
119
239
  declare const _default: FastifyPluginAsync<WebhookPluginOptions>;
120
240
  //#endregion
121
- 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 };
@@ -33,6 +33,13 @@ import fp from "fastify-plugin";
33
33
  /**
34
34
  * Sign a payload with HMAC-SHA256 for outbound webhook delivery.
35
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
+ *
36
43
  * @returns `sha256=<hex>` — the format written to `x-webhook-signature`
37
44
  */
38
45
  function signPayload(payload, secret) {
@@ -41,6 +48,120 @@ function signPayload(payload, secret) {
41
48
  return `sha256=${hmac.digest("hex")}`;
42
49
  }
43
50
  /**
51
+ * Signed-string layout for the v1 webhook contract:
52
+ *
53
+ * v1.<timestampMs>.<deliveryId>.<body>
54
+ *
55
+ * Binding the timestamp bounds replay to the verifier's tolerance window,
56
+ * and binding the delivery id lets receivers deduplicate redeliveries on an
57
+ * AUTHENTICATED value (the legacy `x-webhook-id` header was unsigned — an
58
+ * attacker replaying a captured request could change it and defeat dedup).
59
+ *
60
+ * Wire headers:
61
+ * - `x-arc-webhook-signature: v1=<hex hmac-sha256>`
62
+ * - `x-arc-webhook-timestamp: <unix epoch ms>`
63
+ * - `x-arc-webhook-delivery: <delivery id>` (arc sends the event's
64
+ * `meta.id` — STABLE across retries of the same event, so dedup on it
65
+ * collapses redeliveries)
66
+ */
67
+ function signWebhook(body, secret, meta) {
68
+ const hmac = createHmac("sha256", secret);
69
+ hmac.update(`v1.${meta.timestamp}.${meta.deliveryId}.${body}`);
70
+ return `v1=${hmac.digest("hex")}`;
71
+ }
72
+ /**
73
+ * Verify an inbound v1 arc webhook: signature + timestamp tolerance, and
74
+ * return the authenticated delivery id for receiver-side deduplication.
75
+ *
76
+ * ⚠ Pass `req.rawBody` (route opted in via `config: { rawBody: true }`),
77
+ * never the parsed `req.body` — re-serialised JSON won't match the HMAC.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * fastify.post('/hooks/arc', { config: { rawBody: true } }, async (req, reply) => {
82
+ * const result = verifyWebhook({
83
+ * body: req.rawBody,
84
+ * secret: process.env.WEBHOOK_SECRET,
85
+ * signature: req.headers['x-arc-webhook-signature'] as string,
86
+ * timestamp: req.headers['x-arc-webhook-timestamp'] as string,
87
+ * deliveryId: req.headers['x-arc-webhook-delivery'] as string,
88
+ * });
89
+ * if (!result.valid) return reply.status(401).send({ error: result.reason });
90
+ * if (await seen(result.deliveryId)) return reply.send({ ok: true }); // dedup
91
+ * // ... handle, then mark result.deliveryId seen
92
+ * });
93
+ * ```
94
+ */
95
+ function verifyWebhook(input) {
96
+ const { body, secret, signature, deliveryId, toleranceMs = 3e5, now = Date.now } = input;
97
+ if (typeof body !== "string" && !Buffer.isBuffer(body)) throw new TypeError("verifyWebhook: body must be a string or Buffer (the exact bytes the sender signed). Add `config: { rawBody: true }` to the route and pass req.rawBody — not req.body.");
98
+ if (!signature || !deliveryId || input.timestamp === void 0 || input.timestamp === "") return {
99
+ valid: false,
100
+ reason: "missing_headers"
101
+ };
102
+ const timestamp = Number(input.timestamp);
103
+ if (!Number.isFinite(timestamp)) return {
104
+ valid: false,
105
+ reason: "bad_timestamp"
106
+ };
107
+ if (Math.abs(now() - timestamp) > toleranceMs) return {
108
+ valid: false,
109
+ reason: "expired"
110
+ };
111
+ const expected = signWebhook(typeof body === "string" ? body : body.toString("utf8"), secret, {
112
+ timestamp,
113
+ deliveryId
114
+ });
115
+ const a = Buffer.from(signature);
116
+ const b = Buffer.from(expected);
117
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return {
118
+ valid: false,
119
+ reason: "bad_signature"
120
+ };
121
+ return {
122
+ valid: true,
123
+ deliveryId,
124
+ timestamp
125
+ };
126
+ }
127
+ /**
128
+ * Opt-in URL policy for tenant-registered webhooks: requires HTTPS and
129
+ * rejects loopback, link-local, private-network, and cloud-metadata
130
+ * destinations. Pass as `validateUrl: defaultWebhookUrlPolicy`.
131
+ *
132
+ * Scope note: this validates the URL LITERAL at registration time. It does
133
+ * not resolve DNS — rebinding/level-2 SSRF protection belongs at the actual
134
+ * outbound network boundary (egress proxy, network policy), where the real
135
+ * connection target is known. Internal deployments that legitimately POST
136
+ * to private services should write their own narrower policy instead.
137
+ */
138
+ const BLOCKED_V4 = /^(?:127\.|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.|169\.254\.|0\.)/;
139
+ function defaultWebhookUrlPolicy(url) {
140
+ if (url.protocol !== "https:") throw new Error(`Webhook URL must use https (got ${url.protocol}//)`);
141
+ let host = url.hostname.toLowerCase();
142
+ if (host.startsWith("[") && host.endsWith("]")) host = host.slice(1, -1);
143
+ const blockedName = host === "localhost" || host.endsWith(".localhost") || host === "metadata.google.internal";
144
+ const isV6 = host.includes(":");
145
+ const mappedV4 = isV6 ? decodeMappedV4(host) : void 0;
146
+ const blockedV6 = isV6 && (host === "::1" || host === "::" || /^f[cd]/.test(host) || /^fe[89ab]/.test(host) || mappedV4 !== void 0 && BLOCKED_V4.test(mappedV4));
147
+ const blockedV4 = !isV6 && BLOCKED_V4.test(host);
148
+ if (blockedName || blockedV4 || blockedV6) throw new Error(`Webhook URL host '${host}' is not allowed (loopback/private/metadata)`);
149
+ }
150
+ /**
151
+ * Decode an IPv4-mapped IPv6 literal to its dotted-quad form. Accepts both
152
+ * the textual spelling (`::ffff:1.2.3.4`) and the hex-group canonical form
153
+ * WHATWG URL produces (`::ffff:102:304`). Returns undefined for anything else.
154
+ */
155
+ function decodeMappedV4(host) {
156
+ const dotted = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(host)?.[1];
157
+ if (dotted) return dotted;
158
+ const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
159
+ if (!hex?.[1] || !hex[2]) return void 0;
160
+ const hi = Number.parseInt(hex[1], 16);
161
+ const lo = Number.parseInt(hex[2], 16);
162
+ return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
163
+ }
164
+ /**
44
165
  * Verify an inbound webhook signature using timing-safe comparison.
45
166
  *
46
167
  * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
@@ -133,9 +254,24 @@ const webhookPlugin = async (fastify, opts = {}) => {
133
254
  const timeout = opts.timeout ?? 1e4;
134
255
  const maxLogEntries = opts.maxLogEntries ?? 1e3;
135
256
  const concurrency = opts.concurrency ?? 5;
257
+ if (!Number.isInteger(concurrency) || concurrency < 1) throw new Error(`webhookPlugin: concurrency must be an integer >= 1 (got ${String(opts.concurrency)})`);
136
258
  let subscriptions = [];
137
259
  const log = [];
138
260
  subscriptions = await store.getAll();
261
+ if (opts.validateUrl) {
262
+ const admitted = [];
263
+ for (const sub of subscriptions) try {
264
+ await opts.validateUrl(new URL(sub.url), sub);
265
+ admitted.push(sub);
266
+ } catch (err) {
267
+ fastify.log.warn({
268
+ subscriptionId: sub.id,
269
+ url: sub.url,
270
+ err
271
+ }, "webhookPlugin: persisted subscription failed the URL policy — excluded from dispatch. Fix the URL or unregister the subscription.");
272
+ }
273
+ subscriptions = admitted;
274
+ }
139
275
  async function dispatchEvent(event) {
140
276
  const matching = subscriptions.filter((s) => matchesPattern(s.events, event.type));
141
277
  if (matching.length === 0) return;
@@ -152,7 +288,12 @@ const webhookPlugin = async (fastify, opts = {}) => {
152
288
  timestamp: /* @__PURE__ */ new Date()
153
289
  };
154
290
  try {
155
- const signature = signPayload(body, sub.secret);
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);
156
297
  const ac = new AbortController();
157
298
  const timer = setTimeout(() => ac.abort(), timeout);
158
299
  try {
@@ -160,12 +301,16 @@ const webhookPlugin = async (fastify, opts = {}) => {
160
301
  method: "POST",
161
302
  headers: {
162
303
  "content-type": "application/json",
163
- "x-webhook-signature": signature,
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,
164
308
  "x-webhook-id": event.meta.id,
165
309
  "x-webhook-event": event.type
166
310
  },
167
311
  body,
168
- signal: ac.signal
312
+ signal: ac.signal,
313
+ ...opts.validateUrl ? { redirect: "manual" } : {}
169
314
  });
170
315
  record.success = response.ok;
171
316
  record.status = response.status;
@@ -188,6 +333,13 @@ const webhookPlugin = async (fastify, opts = {}) => {
188
333
  if (fastify.events) unsubscribe = await fastify.events.subscribe("*", dispatchEvent);
189
334
  fastify.decorate("webhooks", {
190
335
  async register(sub) {
336
+ let parsed;
337
+ try {
338
+ parsed = new URL(sub.url);
339
+ } catch {
340
+ throw new Error(`webhookPlugin: invalid webhook URL '${sub.url}'`);
341
+ }
342
+ if (opts.validateUrl) await opts.validateUrl(parsed, sub);
191
343
  await store.save(sub);
192
344
  subscriptions = subscriptions.filter((s) => s.id !== sub.id);
193
345
  subscriptions.push(sub);
@@ -197,7 +349,10 @@ const webhookPlugin = async (fastify, opts = {}) => {
197
349
  subscriptions = subscriptions.filter((s) => s.id !== id);
198
350
  },
199
351
  list() {
200
- return [...subscriptions];
352
+ return subscriptions.map(({ secret: _secret, ...pub }) => pub);
353
+ },
354
+ getSecret(id) {
355
+ return subscriptions.find((s) => s.id === id)?.secret;
201
356
  },
202
357
  deliveryLog(limit) {
203
358
  if (limit) return log.slice(-limit);
@@ -217,4 +372,4 @@ var webhooks_default = fp(webhookPlugin, {
217
372
  dependencies: ["arc-events"]
218
373
  });
219
374
  //#endregion
220
- export { webhooks_default as default, signPayload, verifySignature, webhookPlugin };
375
+ export { webhooks_default as default, defaultWebhookUrlPolicy, signPayload, signWebhook, verifySignature, verifyWebhook, webhookPlugin };
@@ -0,0 +1,16 @@
1
+ //#region src/idempotency/stores/interface.ts
2
+ /**
3
+ * Helper to create a result object
4
+ */
5
+ function createIdempotencyResult(statusCode, body, headers, ttlMs) {
6
+ const now = /* @__PURE__ */ new Date();
7
+ return {
8
+ statusCode,
9
+ headers,
10
+ body,
11
+ createdAt: now,
12
+ expiresAt: new Date(now.getTime() + ttlMs)
13
+ };
14
+ }
15
+ //#endregion
16
+ export { createIdempotencyResult as t };
@@ -1,4 +1,4 @@
1
- import { t as ResourceRegistry } from "./ResourceRegistry-7RnrEWNP.mjs";
1
+ import { t as ResourceRegistry } from "./ResourceRegistry-y9Hnypus.mjs";
2
2
  import { resolve } from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  //#region src/cli/utils/loadResourcesFromEntry.ts