@classytic/arc 2.8.5 → 2.9.1

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 (140) hide show
  1. package/README.md +88 -5
  2. package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-Vu2yc56T.mjs} +188 -102
  3. package/dist/EventTransport-CqZ8FyM_.d.mts +293 -0
  4. package/dist/adapters/index.d.mts +2 -2
  5. package/dist/audit/index.d.mts +100 -11
  6. package/dist/audit/index.mjs +71 -18
  7. package/dist/auth/index.d.mts +16 -8
  8. package/dist/auth/index.mjs +13 -6
  9. package/dist/auth/redis-session.d.mts +1 -1
  10. package/dist/{betterAuthOpenApi-BuUcUEJq.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
  11. package/dist/cache/index.d.mts +2 -2
  12. package/dist/cache/index.mjs +2 -2
  13. package/dist/cli/commands/docs.mjs +2 -2
  14. package/dist/cli/commands/introspect.mjs +1 -1
  15. package/dist/core/index.d.mts +3 -3
  16. package/dist/core/index.mjs +4 -5
  17. package/dist/{core-F0QoWBt2.mjs → core-DNncu0xF.mjs} +1 -1
  18. package/dist/{createActionRouter-BORM8f17.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
  19. package/dist/{createApp-B1EY8zxa.mjs → createApp-CBJUJKGP.mjs} +13 -12
  20. package/dist/{defineResource-tcgySDo1.mjs → defineResource-C__jkwvs.mjs} +22 -57
  21. package/dist/docs/index.d.mts +2 -2
  22. package/dist/docs/index.mjs +1 -1
  23. package/dist/dynamic/index.d.mts +1 -1
  24. package/dist/dynamic/index.mjs +3 -3
  25. package/dist/{elevation-DtFxrG0s.mjs → elevation-DxQ6ACbt.mjs} +21 -7
  26. package/dist/{errorHandler-f869_8PQ.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
  27. package/dist/{errorHandler-Bah5JhBd.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
  28. package/dist/{eventPlugin-D9DKB2zM.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
  29. package/dist/{eventPlugin-CDjVTM82.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
  30. package/dist/events/index.d.mts +147 -36
  31. package/dist/events/index.mjs +338 -101
  32. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  33. package/dist/events/transports/redis.d.mts +1 -1
  34. package/dist/factory/index.d.mts +1 -1
  35. package/dist/factory/index.mjs +2 -2
  36. package/dist/{fields-DpZQa_Q3.d.mts → fields-BC7zcmI9.d.mts} +15 -3
  37. package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
  38. package/dist/{filesUpload-C7r7HIeA.mjs → filesUpload-q8oHt--L.mjs} +65 -7
  39. package/dist/hooks/index.d.mts +1 -1
  40. package/dist/hooks/index.mjs +1 -1
  41. package/dist/idempotency/index.d.mts +29 -5
  42. package/dist/idempotency/index.mjs +111 -2
  43. package/dist/idempotency/redis.d.mts +1 -1
  44. package/dist/{index-BLXBmWud.d.mts → index-C-xjcA6F.d.mts} +1 -1
  45. package/dist/{index-DtDzOBn8.d.mts → index-Cibkchnx.d.mts} +3 -134
  46. package/dist/{index-C1meYuDn.d.mts → index-CtGKT0lf.d.mts} +1 -1
  47. package/dist/index.d.mts +7 -7
  48. package/dist/index.mjs +9 -9
  49. package/dist/integrations/event-gateway.d.mts +1 -1
  50. package/dist/integrations/event-gateway.mjs +1 -1
  51. package/dist/integrations/index.d.mts +1 -1
  52. package/dist/integrations/mcp/index.d.mts +26 -8
  53. package/dist/integrations/mcp/index.mjs +96 -17
  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 +5 -0
  57. package/dist/integrations/webhooks.mjs +6 -0
  58. package/dist/{interface-CMRutPfe.d.mts → interface-YrWsmKqE.d.mts} +287 -179
  59. package/dist/{openapi-CbKUJY_m.mjs → openapi-CXuTG1M9.mjs} +2 -2
  60. package/dist/org/index.d.mts +1 -1
  61. package/dist/permissions/index.d.mts +2 -2
  62. package/dist/permissions/index.mjs +3 -3
  63. package/dist/{permissions-CH4cNwJi.mjs → permissions-oNZawnkR.mjs} +1 -1
  64. package/dist/plugins/index.d.mts +7 -7
  65. package/dist/plugins/index.mjs +11 -11
  66. package/dist/plugins/response-cache.mjs +1 -1
  67. package/dist/plugins/tracing-entry.d.mts +1 -1
  68. package/dist/plugins/tracing-entry.mjs +1 -1
  69. package/dist/policies/index.d.mts +25 -32
  70. package/dist/presets/filesUpload.d.mts +26 -4
  71. package/dist/presets/filesUpload.mjs +1 -1
  72. package/dist/presets/index.d.mts +3 -2
  73. package/dist/presets/index.mjs +4 -3
  74. package/dist/presets/multiTenant.d.mts +1 -1
  75. package/dist/presets/multiTenant.mjs +1 -1
  76. package/dist/presets/search.d.mts +91 -0
  77. package/dist/presets/search.mjs +150 -0
  78. package/dist/{presets-C2xgzW6x.mjs → presets-hM4WhNWY.mjs} +1 -1
  79. package/dist/{queryCachePlugin-BJJGBTlu.d.mts → queryCachePlugin-CnTZZTC5.d.mts} +1 -1
  80. package/dist/{queryCachePlugin-BH-fidlv.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
  81. package/dist/{redis-BM00zaPB.d.mts → redis-MXLp1oOf.d.mts} +1 -1
  82. package/dist/{redis-stream-CrsfUmPt.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
  83. package/dist/registry/index.d.mts +1 -1
  84. package/dist/registry/index.mjs +2 -2
  85. package/dist/{resourceToTools-8s-EsCCe.mjs → resourceToTools-C3cWymnW.mjs} +64 -47
  86. package/dist/rpc/index.d.mts +1 -1
  87. package/dist/rpc/index.mjs +1 -1
  88. package/dist/{schemaConverter-Y7nCYaLJ.mjs → schemaConverter-BxFDdtXu.mjs} +1 -1
  89. package/dist/scope/index.mjs +1 -1
  90. package/dist/{sse-Ad7ypl9e.mjs → sse-CJpt7LGI.mjs} +1 -1
  91. package/dist/store-helpers-DFiZl5TL.mjs +57 -0
  92. package/dist/testing/index.d.mts +5 -14
  93. package/dist/testing/index.mjs +21 -75
  94. package/dist/testing/storageContract.d.mts +1 -1
  95. package/dist/types/index.d.mts +2 -2
  96. package/dist/types/storage.d.mts +1 -1
  97. package/dist/{types-BsbNMEDR.d.mts → types-CoSzA-s-.d.mts} +1 -1
  98. package/dist/{types-Ch9pTQbf.d.mts → types-CunEX4UX.d.mts} +10 -8
  99. package/dist/utils/index.d.mts +4 -4
  100. package/dist/utils/index.mjs +6 -6
  101. package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
  102. package/package.json +8 -11
  103. package/skills/arc/SKILL.md +92 -14
  104. package/skills/arc/references/auth.md +94 -0
  105. package/skills/arc/references/events.md +200 -12
  106. package/skills/arc/references/mcp.md +4 -17
  107. package/skills/arc/references/multi-tenancy.md +43 -0
  108. package/skills/arc/references/production.md +34 -19
  109. package/dist/EventTransport-BXja8NOc.d.mts +0 -135
  110. package/dist/audit/mongodb.d.mts +0 -2
  111. package/dist/audit/mongodb.mjs +0 -2
  112. package/dist/idempotency/mongodb.d.mts +0 -2
  113. package/dist/idempotency/mongodb.mjs +0 -123
  114. package/dist/mongodb-BsP-WbhN.d.mts +0 -127
  115. package/dist/mongodb-CTcp0hQZ.d.mts +0 -80
  116. package/dist/mongodb-Utc5k_-0.mjs +0 -90
  117. /package/dist/{HookSystem-HprTmvVY.mjs → HookSystem-BjFu7zf1.mjs} +0 -0
  118. /package/dist/{ResourceRegistry-C6uXlWe3.mjs → ResourceRegistry-Dq3_zBQP.mjs} +0 -0
  119. /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
  120. /package/dist/{caching-IMuYVjTL.mjs → caching-CjybdRwx.mjs} +0 -0
  121. /package/dist/{circuitBreaker-dTtG-UyS.d.mts → circuitBreaker-CvXkjfrW.d.mts} +0 -0
  122. /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
  123. /package/dist/{errors-Ck2h67pm.d.mts → errors-BI8kEKsO.d.mts} +0 -0
  124. /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
  125. /package/dist/{externalPaths-BnkYrNzp.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  126. /package/dist/{interface-DfLGcus7.d.mts → interface-B-pe8fhj.d.mts} +0 -0
  127. /package/dist/{interface-4y979v99.d.mts → interface-DplgQO2e.d.mts} +0 -0
  128. /package/dist/{loadResources-PWd0OCpV.mjs → loadResources-Bksk8ydA.mjs} +0 -0
  129. /package/dist/{logger-D1YrIImS.mjs → logger-CDjpjySd.mjs} +0 -0
  130. /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
  131. /package/dist/{metrics-B-PU4-Yu.mjs → metrics-TuOmguhi.mjs} +0 -0
  132. /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
  133. /package/dist/{registry-BiTKT1Dg.mjs → registry-B0Wl7uVV.mjs} +0 -0
  134. /package/dist/{replyHelpers-CxkYGT81.mjs → replyHelpers-BLojtuvR.mjs} +0 -0
  135. /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
  136. /package/dist/{sessionManager-DDCmiNIo.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  137. /package/dist/{storage-Dfzt4VTl.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  138. /package/dist/{tracing-DdN2-wHJ.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
  139. /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
  140. /package/dist/{versioning-CDugduqI.mjs → versioning-Cm8qoFDg.mjs} +0 -0
@@ -1,5 +1,5 @@
1
- import { qt as ResourceDefinition } from "../../interface-CMRutPfe.mjs";
2
- import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-BsbNMEDR.mjs";
1
+ import { Kt as ResourceDefinition } from "../../interface-YrWsmKqE.mjs";
2
+ import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler, u as PromptResult } from "../../types-CoSzA-s-.mjs";
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { z } from "zod";
5
5
 
@@ -233,14 +233,32 @@ declare class McpSessionCache {
233
233
  }
234
234
  //#endregion
235
235
  //#region src/integrations/mcp/mcpPlugin.d.ts
236
+ /**
237
+ * Per-prefix MCP registration info. A single fastify instance can register
238
+ * mcpPlugin multiple times under different prefixes (e.g. `/mcp/catalog`,
239
+ * `/mcp/orders`). The decorator is a map keyed by prefix so multi-registration
240
+ * setups can inspect any endpoint's tool list, not just the first one.
241
+ */
242
+ interface McpRegistration {
243
+ sessions: McpSessionCache | null;
244
+ toolNames: string[];
245
+ resourceNames: string[];
246
+ stateful: boolean;
247
+ }
248
+ interface McpDecorator {
249
+ /** Map of prefix → registration info. Iterate for all endpoints. */
250
+ registrations: Map<string, McpRegistration>;
251
+ /** Shortcut for the first registered prefix (back-compat for single-endpoint apps) */
252
+ readonly sessions: McpSessionCache | null;
253
+ readonly toolNames: string[];
254
+ readonly resourceNames: string[];
255
+ readonly stateful: boolean;
256
+ /** Look up a specific prefix */
257
+ get(prefix: string): McpRegistration | undefined;
258
+ }
236
259
  declare module "fastify" {
237
260
  interface FastifyInstance {
238
- mcp?: {
239
- sessions: McpSessionCache | null;
240
- toolNames: string[];
241
- resourceNames: string[];
242
- stateful: boolean;
243
- };
261
+ mcp?: McpDecorator;
244
262
  }
245
263
  }
246
264
  declare const mcpPlugin: FastifyPluginAsync<McpPluginOptions>;
@@ -1,5 +1,5 @@
1
- import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-8s-EsCCe.mjs";
2
- import { createHash } from "node:crypto";
1
+ import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-C3cWymnW.mjs";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
4
  //#region src/integrations/mcp/defineTool.ts
5
5
  /**
@@ -454,13 +454,46 @@ var McpSessionCache = class {
454
454
  };
455
455
  //#endregion
456
456
  //#region src/integrations/mcp/mcpPlugin.ts
457
+ /**
458
+ * @classytic/arc — MCP Plugin (Level 1)
459
+ *
460
+ * Fastify plugin that auto-generates MCP tools from Arc resources.
461
+ *
462
+ * Two transport modes:
463
+ * - **Stateless** (default) — fresh server per request, no session tracking.
464
+ * Best for production, horizontal scaling, serverless, edge.
465
+ * - **Stateful** — sessions cached with TTL, reused across requests.
466
+ * Use when you need server-initiated notifications or long-lived connections.
467
+ *
468
+ * Auth is NOT enforced — the plugin respects whatever auth mode you choose:
469
+ * - `auth: false` — no auth, anonymous access (dev/testing/stdio)
470
+ * - `auth: betterAuthInstance` — OAuth 2.1 via Better Auth's mcp() plugin
471
+ * - `auth: async (headers) => {...}` — custom function (API key, JWT, gateway, etc.)
472
+ *
473
+ * @example
474
+ * ```typescript
475
+ * // Stateless (default) — production, scalable
476
+ * await app.register(mcpPlugin, { resources, auth: false });
477
+ *
478
+ * // Stateful — when you need session persistence
479
+ * await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
480
+ *
481
+ * // Multiple MCP endpoints scoped to different resource groups
482
+ * await app.register(mcpPlugin, { resources: catalogResources, prefix: '/mcp/catalog' });
483
+ * await app.register(mcpPlugin, { resources: orderResources, prefix: '/mcp/orders' });
484
+ * ```
485
+ */
457
486
  const mcpPluginImpl = async (fastify, options) => {
458
487
  let StreamableHTTPServerTransport;
459
488
  try {
460
489
  StreamableHTTPServerTransport = (await import("@modelcontextprotocol/sdk/server/streamableHttp.js")).StreamableHTTPServerTransport;
490
+ } catch {
491
+ throw new Error("@modelcontextprotocol/sdk is required for MCP support. Install it: npm install @modelcontextprotocol/sdk");
492
+ }
493
+ try {
461
494
  await import("zod");
462
495
  } catch {
463
- throw new Error("@modelcontextprotocol/sdk and zod are required for MCP support. Install them: npm install @modelcontextprotocol/sdk zod");
496
+ throw new Error("zod is required for MCP tool schemas. Install it: npm install zod");
464
497
  }
465
498
  let enabledResources;
466
499
  if (options.include) {
@@ -516,18 +549,51 @@ const mcpPluginImpl = async (fastify, options) => {
516
549
  registerStatelessRoutes(fastify, prefix, options, createServerInstance, StreamableHTTPServerTransport, authCache);
517
550
  }
518
551
  if (cache) fastify.addHook("onClose", async () => cache.close());
519
- if (!fastify.hasDecorator("mcp")) fastify.decorate("mcp", {
552
+ const registration = {
520
553
  sessions: cache,
521
554
  toolNames: allTools.map((t) => t.name),
522
555
  resourceNames: enabledResources.map((r) => r.name),
523
556
  stateful
524
- });
557
+ };
558
+ if (!fastify.hasDecorator("mcp")) {
559
+ const registrations = /* @__PURE__ */ new Map();
560
+ registrations.set(prefix, registration);
561
+ const first = () => registrations.values().next().value;
562
+ const decorator = {
563
+ registrations,
564
+ get(p) {
565
+ return registrations.get(p);
566
+ },
567
+ get sessions() {
568
+ return first()?.sessions ?? null;
569
+ },
570
+ get toolNames() {
571
+ return first()?.toolNames ?? [];
572
+ },
573
+ get resourceNames() {
574
+ return first()?.resourceNames ?? [];
575
+ },
576
+ get stateful() {
577
+ return first()?.stateful ?? false;
578
+ }
579
+ };
580
+ fastify.decorate("mcp", decorator);
581
+ } else {
582
+ const existing = fastify.mcp;
583
+ if (existing) {
584
+ if (existing.registrations.has(prefix)) throw new Error(`mcpPlugin: prefix "${prefix}" is already registered`);
585
+ existing.registrations.set(prefix, registration);
586
+ }
587
+ }
525
588
  };
526
589
  function registerStatelessRoutes(fastify, prefix, options, createServer, Transport, authCache) {
527
590
  fastify.post(prefix, async (request, reply) => {
528
591
  const authResult = await resolveMcpAuth(request.headers, options.auth ?? false, authCache);
529
592
  if (!authResult && options.auth) {
530
- fastify.log.warn("mcpPlugin: auth failed — returning 401 (client may silently drop server)");
593
+ fastify.log.warn({
594
+ msg: "mcpPlugin: auth failed",
595
+ status: 401
596
+ });
531
597
  return reply.code(401).send({
532
598
  jsonrpc: "2.0",
533
599
  error: {
@@ -564,7 +630,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
564
630
  fastify.post(prefix, async (request, reply) => {
565
631
  const authResult = await resolveMcpAuth(request.headers, options.auth ?? false);
566
632
  if (!authResult && options.auth) {
567
- fastify.log.warn("mcpPlugin: auth failed — returning 401 (client may silently drop server)");
633
+ fastify.log.warn({
634
+ msg: "mcpPlugin: auth failed",
635
+ status: 401
636
+ });
568
637
  return reply.code(401).send({
569
638
  jsonrpc: "2.0",
570
639
  error: {
@@ -592,15 +661,19 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
592
661
  }
593
662
  }
594
663
  const authRef = { current: authResult };
595
- const transport = new Transport({ sessionIdGenerator: void 0 });
596
- await (await createServer(authRef)).connect(transport);
597
- cache.set(transport.sessionId, {
598
- transport,
599
- lastAccessed: Date.now(),
600
- organizationId: authResult?.organizationId ?? "",
601
- auth: authResult,
602
- authRef
664
+ const transport = new Transport({
665
+ sessionIdGenerator: () => randomUUID(),
666
+ onsessioninitialized: (newSessionId) => {
667
+ cache.set(newSessionId, {
668
+ transport,
669
+ lastAccessed: Date.now(),
670
+ organizationId: authResult?.organizationId ?? "",
671
+ auth: authResult,
672
+ authRef
673
+ });
674
+ }
603
675
  });
676
+ await (await createServer(authRef)).connect(transport);
604
677
  await transport.handleRequest(request.raw, reply.raw, request.body);
605
678
  });
606
679
  fastify.get(prefix, async (request, reply) => {
@@ -609,7 +682,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
609
682
  const entry = cache.get(sessionId);
610
683
  if (!entry) return reply.code(403).send({ error: "Unauthorized" });
611
684
  if (options.auth) {
612
- if (!isSessionOwner(entry, await resolveMcpAuth(request.headers, options.auth))) return reply.code(403).send({ error: "Unauthorized" });
685
+ const authResult = await resolveMcpAuth(request.headers, options.auth);
686
+ if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
687
+ entry.auth = authResult;
688
+ entry.authRef.current = authResult;
613
689
  }
614
690
  cache.touch(sessionId);
615
691
  await entry.transport.handleRequest(request.raw, reply.raw);
@@ -620,7 +696,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
620
696
  const entry = cache.get(sessionId);
621
697
  if (!entry) return reply.code(204).send();
622
698
  if (options.auth) {
623
- if (!isSessionOwner(entry, await resolveMcpAuth(request.headers, options.auth))) return reply.code(403).send({ error: "Unauthorized" });
699
+ const authResult = await resolveMcpAuth(request.headers, options.auth);
700
+ if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
701
+ entry.auth = authResult;
702
+ entry.authRef.current = authResult;
624
703
  }
625
704
  cache.remove(sessionId);
626
705
  reply.code(204).send();
@@ -1,4 +1,4 @@
1
- import { o as McpAuthResult, s as McpPluginOptions } from "../../types-BsbNMEDR.mjs";
1
+ import { o as McpAuthResult, s as McpPluginOptions } from "../../types-CoSzA-s-.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
@@ -1,4 +1,4 @@
1
- import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-8s-EsCCe.mjs";
1
+ import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-C3cWymnW.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -76,6 +76,11 @@ interface VerifySignatureOptions {
76
76
  * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
77
77
  * but configurable for any HMAC scheme via options.
78
78
  *
79
+ * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
80
+ * parses JSON bodies by default and the re-serialised object will NOT match
81
+ * the original signature. Register `@fastify/raw-body` or use `preParsing`
82
+ * to capture `req.rawBody`, then pass that here.
83
+ *
79
84
  * @param body - Raw request body (string or Buffer — must be the exact bytes the sender signed)
80
85
  * @param secret - Shared secret between sender and receiver
81
86
  * @param signature - The signature header value (e.g. `req.headers['x-webhook-signature']`)
@@ -46,6 +46,11 @@ function signPayload(payload, secret) {
46
46
  * Works with Arc's own `signPayload` format by default (`sha256=<hex>`),
47
47
  * but configurable for any HMAC scheme via options.
48
48
  *
49
+ * ⚠ The `body` argument must be the exact bytes the sender signed. Fastify
50
+ * parses JSON bodies by default and the re-serialised object will NOT match
51
+ * the original signature. Register `@fastify/raw-body` or use `preParsing`
52
+ * to capture `req.rawBody`, then pass that here.
53
+ *
49
54
  * @param body - Raw request body (string or Buffer — must be the exact bytes the sender signed)
50
55
  * @param secret - Shared secret between sender and receiver
51
56
  * @param signature - The signature header value (e.g. `req.headers['x-webhook-signature']`)
@@ -77,6 +82,7 @@ function signPayload(payload, secret) {
77
82
  * ```
78
83
  */
79
84
  function verifySignature(body, secret, signature, options) {
85
+ if (typeof body !== "string" && !Buffer.isBuffer(body)) throw new TypeError("verifySignature: body must be a string or Buffer (the exact bytes the sender signed). Register @fastify/raw-body and pass req.rawBody — not req.body.");
80
86
  if (!signature) return false;
81
87
  const prefix = options?.prefix ?? "sha256=";
82
88
  const algorithm = options?.algorithm ?? "sha256";