{"_id":"@daxmadov/hmac-kit","_rev":"3-07f792ffdf9a2eb105baa709ee182fba","name":"@daxmadov/hmac-kit","dist-tags":{"latest":"0.2.1"},"versions":{"0.1.0":{"name":"@daxmadov/hmac-kit","version":"0.1.0","keywords":["hmac","auth","authentication","signature","request-signing","security","api","express","nestjs","fastify"],"author":{"name":"daxmadov"},"license":"MIT","_id":"@daxmadov/hmac-kit@0.1.0","maintainers":[{"name":"dima14","email":"axmadovdilmurod98@gmail.com"}],"homepage":"https://github.com/DilmurodAxmadov/hmac-kit#readme","bugs":{"url":"https://github.com/DilmurodAxmadov/hmac-kit/issues"},"dist":{"shasum":"563f855789c03d227dcec899af1a634da1c93523","tarball":"https://registry.npmjs.org/@daxmadov/hmac-kit/-/hmac-kit-0.1.0.tgz","fileCount":46,"integrity":"sha512-D4tUX5IDA63yWSnJhFjo03bOanm4VMDE/a5GMcewoh+xRWjg+HLyUVjEjAFZZud4LwPO6QTzs9xNl6B+rbF+qA==","signatures":[{"sig":"MEUCIEOa7AriDyvOgIY7n7NxjOWdMU3vG4Cq267c5GaqDHHCAiEAkAuF8WC9QLduO0+2K4H95uuoyihgtJRAM3SM0YDJJzo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":476895},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js","require":"./dist/client/index.cjs"},"./server":{"types":"./dist/server/index.d.ts","import":"./dist/server/index.js","require":"./dist/server/index.cjs"},"./package.json":"./package.json","./adapters/nestjs":{"types":"./dist/adapters/nestjs.d.ts","import":"./dist/adapters/nestjs.js","require":"./dist/adapters/nestjs.cjs"},"./adapters/express":{"types":"./dist/adapters/express.d.ts","import":"./dist/adapters/express.js","require":"./dist/adapters/express.cjs"},"./adapters/fastify":{"types":"./dist/adapters/fastify.d.ts","import":"./dist/adapters/fastify.js","require":"./dist/adapters/fastify.cjs"}},"gitHead":"afa45cf777ce22b73205be4d840b4e2bb4c3d041","scripts":{"dev":"tsup --watch","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"dima14","email":"axmadovdilmurod98@gmail.com"},"repository":{"url":"git+https://github.com/DilmurodAxmadov/hmac-kit.git","type":"git"},"_npmVersion":"11.9.0","description":"Framework-agnostic HMAC-SHA256 request signing for server-to-server authentication. Includes client signer, server verifier, nonce stores (memory/Redis), and Express/NestJS/Fastify adapters.","directories":{},"sideEffects":false,"_nodeVersion":"25.6.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","tsup":"^8.0.0","eslint":"^8.56.0","vitest":"^1.2.0","express":"^4.18.2","fastify":"^4.26.0","ioredis":"^5.3.2","prettier":"^3.2.0","typescript":"^5.3.0","@types/node":"^20.11.0","@nestjs/common":"^10.0.0","@types/express":"^4.17.21","@vitest/coverage-v8":"^1.2.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^7.0.0","@typescript-eslint/eslint-plugin":"^7.0.0"},"peerDependencies":{"rxjs":"^7.0.0","express":"^4.0.0","fastify":"^4.0.0","ioredis":"^5.0.0","@nestjs/common":"^10.0.0"},"peerDependenciesMeta":{"rxjs":{"optional":true},"express":{"optional":true},"fastify":{"optional":true},"ioredis":{"optional":true},"@nestjs/common":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/hmac-kit_0.1.0_1777888117988_0.5128140833392287","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@daxmadov/hmac-kit","version":"0.2.0","keywords":["hmac","auth","authentication","signature","request-signing","security","api","express","nestjs","fastify"],"author":{"name":"daxmadov"},"license":"MIT","_id":"@daxmadov/hmac-kit@0.2.0","maintainers":[{"name":"dima14","email":"axmadovdilmurod98@gmail.com"}],"homepage":"https://github.com/DilmurodAxmadov/hmac-kit#readme","bugs":{"url":"https://github.com/DilmurodAxmadov/hmac-kit/issues"},"dist":{"shasum":"12898321b75d8978d4afdf59e1c93cbb2329b569","tarball":"https://registry.npmjs.org/@daxmadov/hmac-kit/-/hmac-kit-0.2.0.tgz","fileCount":56,"integrity":"sha512-uBEvCZ/BglpllJplqvppuTmUcREmA0uPYYtkH8I/ldFIIhZHTmmOcTYRvreQQBr5ipYNzLtE4QUM9meiF7NJmw==","signatures":[{"sig":"MEQCIDBGUmyMXpACVtpST5Mye9FIEKRff8YmiMYYuBcJecNLAiAqagtvYNuWp/Dz90UWef+M8PTbxz9o3Itkw0Jcy1UqLg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":649780},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./edge":{"types":"./dist/edge/index.d.ts","import":"./dist/edge/index.js","require":"./dist/edge/index.cjs"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js","require":"./dist/client/index.cjs"},"./server":{"types":"./dist/server/index.d.ts","import":"./dist/server/index.js","require":"./dist/server/index.cjs"},"./package.json":"./package.json","./adapters/nestjs":{"types":"./dist/adapters/nestjs.d.ts","import":"./dist/adapters/nestjs.js","require":"./dist/adapters/nestjs.cjs"},"./adapters/express":{"types":"./dist/adapters/express.d.ts","import":"./dist/adapters/express.js","require":"./dist/adapters/express.cjs"},"./adapters/fastify":{"types":"./dist/adapters/fastify.d.ts","import":"./dist/adapters/fastify.js","require":"./dist/adapters/fastify.cjs"}},"gitHead":"5a42ff191b3700ec0ed72bea7b4dca3f45e6b0f7","scripts":{"dev":"tsup --watch","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","test":"vitest run","build":"tsup","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"_npmUser":{"name":"dima14","email":"axmadovdilmurod98@gmail.com"},"repository":{"url":"git+https://github.com/DilmurodAxmadov/hmac-kit.git","type":"git"},"_npmVersion":"11.9.0","description":"Framework-agnostic HMAC-SHA256 request signing for server-to-server authentication. Includes client signer, server verifier, nonce stores (memory/Redis), and Express/NestJS/Fastify adapters.","directories":{},"sideEffects":false,"_nodeVersion":"25.6.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","tsup":"^8.0.0","eslint":"^8.56.0","vitest":"^1.2.0","express":"^4.18.2","fastify":"^4.26.0","ioredis":"^5.3.2","prettier":"^3.2.0","typescript":"^5.3.0","@types/node":"^20.11.0","@nestjs/common":"^10.0.0","@types/express":"^4.17.21","@vitest/coverage-v8":"^1.2.0","eslint-config-prettier":"^9.1.0","@typescript-eslint/parser":"^7.0.0","@typescript-eslint/eslint-plugin":"^7.0.0"},"peerDependencies":{"rxjs":"^7.0.0","express":"^4.0.0","fastify":"^4.0.0","ioredis":"^5.0.0","@nestjs/common":"^10.0.0"},"peerDependenciesMeta":{"rxjs":{"optional":true},"express":{"optional":true},"fastify":{"optional":true},"ioredis":{"optional":true},"@nestjs/common":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/hmac-kit_0.2.0_1778150564914_0.6874630909539103","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@daxmadov/hmac-kit","version":"0.2.1","description":"Framework-agnostic HMAC-SHA256 request signing for server-to-server authentication. Includes client signer, server verifier, nonce stores (memory/Redis), and Express/NestJS/Fastify adapters.","keywords":["hmac","auth","authentication","signature","request-signing","security","api","express","nestjs","fastify"],"license":"MIT","author":{"name":"daxmadov"},"repository":{"type":"git","url":"git+https://github.com/DilmurodAxmadov/hmac-kit.git"},"bugs":{"url":"https://github.com/DilmurodAxmadov/hmac-kit/issues"},"homepage":"https://github.com/DilmurodAxmadov/hmac-kit#readme","type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./client":{"types":"./dist/client/index.d.ts","import":"./dist/client/index.js","require":"./dist/client/index.cjs"},"./server":{"types":"./dist/server/index.d.ts","import":"./dist/server/index.js","require":"./dist/server/index.cjs"},"./adapters/express":{"types":"./dist/adapters/express.d.ts","import":"./dist/adapters/express.js","require":"./dist/adapters/express.cjs"},"./adapters/nestjs":{"types":"./dist/adapters/nestjs.d.ts","import":"./dist/adapters/nestjs.js","require":"./dist/adapters/nestjs.cjs"},"./adapters/fastify":{"types":"./dist/adapters/fastify.d.ts","import":"./dist/adapters/fastify.js","require":"./dist/adapters/fastify.cjs"},"./edge":{"types":"./dist/edge/index.d.ts","import":"./dist/edge/index.js","require":"./dist/edge/index.cjs"},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","lint":"eslint \"src/**/*.ts\" \"tests/**/*.ts\"","format":"prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"","prepublishOnly":"npm run lint && npm run typecheck && npm run test && npm run build"},"engines":{"node":">=18"},"devDependencies":{"@nestjs/common":"^10.0.0","@types/express":"^4.17.21","@types/node":"^20.11.0","@typescript-eslint/eslint-plugin":"^7.0.0","@typescript-eslint/parser":"^7.0.0","@vitest/coverage-v8":"^1.2.0","eslint":"^8.56.0","eslint-config-prettier":"^9.1.0","express":"^4.18.2","fastify":"^4.26.0","ioredis":"^5.3.2","prettier":"^3.2.0","rxjs":"^7.8.1","tsup":"^8.0.0","typescript":"^5.3.0","vitest":"^1.2.0"},"peerDependencies":{"@nestjs/common":"^10.0.0","express":"^4.0.0","fastify":"^4.0.0","ioredis":"^5.0.0","rxjs":"^7.0.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true},"express":{"optional":true},"fastify":{"optional":true},"ioredis":{"optional":true},"rxjs":{"optional":true}},"publishConfig":{"access":"public"},"gitHead":"1c23bb49bdc918bd81d2737e5b9ef7ff6743053d","_id":"@daxmadov/hmac-kit@0.2.1","_nodeVersion":"25.6.1","_npmVersion":"11.9.0","dist":{"integrity":"sha512-M4azgP0E92xcpfwF8sGrUAoohLVAixsjJRyH2GuoOrhpSJi1nHXjxnZ+NVr1VBn63949QXrml8gFe+bTurJaWQ==","shasum":"a21b421416e9b6ee4f11b3697d2d8d511e925752","tarball":"https://registry.npmjs.org/@daxmadov/hmac-kit/-/hmac-kit-0.2.1.tgz","fileCount":56,"unpackedSize":680322,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIGwFLM0yU88GxkX22etE8T52QMoy9npHsaBswRAiImj9AiB4O3lYkSCudh/T6mPOJ5xsfXqbNBuaR+VhMgXQm5GG/A=="}]},"_npmUser":{"name":"dima14","email":"axmadovdilmurod98@gmail.com"},"directories":{},"maintainers":[{"name":"dima14","email":"axmadovdilmurod98@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hmac-kit_0.2.1_1779087258806_0.49445949289258917"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-04T09:48:37.870Z","modified":"2026-05-18T06:54:19.104Z","0.1.0":"2026-05-04T09:48:38.159Z","0.2.0":"2026-05-07T10:42:45.112Z","0.2.1":"2026-05-18T06:54:18.995Z"},"bugs":{"url":"https://github.com/DilmurodAxmadov/hmac-kit/issues"},"author":{"name":"daxmadov"},"license":"MIT","homepage":"https://github.com/DilmurodAxmadov/hmac-kit#readme","keywords":["hmac","auth","authentication","signature","request-signing","security","api","express","nestjs","fastify"],"repository":{"type":"git","url":"git+https://github.com/DilmurodAxmadov/hmac-kit.git"},"description":"Framework-agnostic HMAC-SHA256 request signing for server-to-server authentication. Includes client signer, server verifier, nonce stores (memory/Redis), and Express/NestJS/Fastify adapters.","maintainers":[{"name":"dima14","email":"axmadovdilmurod98@gmail.com"}],"readme":"# @daxmadov/hmac-kit\n\n[![npm version](https://img.shields.io/npm/v/@daxmadov/hmac-kit.svg)](https://www.npmjs.com/package/@daxmadov/hmac-kit)\n[![npm downloads](https://img.shields.io/npm/dm/@daxmadov/hmac-kit.svg)](https://www.npmjs.com/package/@daxmadov/hmac-kit)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Node.js Version](https://img.shields.io/node/v/@daxmadov/hmac-kit.svg)](https://nodejs.org)\n\nFramework-agnostic HMAC-SHA256/SHA-512 request signing for server-to-server\nauthentication. Includes a stateless client signer, a server-side verifier\nwith replay protection, pluggable nonce storage (memory / Redis), and\nadapters for Express, Fastify, and NestJS.\n\n- Constant-time signature comparison\n- Replay protection via per-client nonces\n- 5-minute timestamp window (configurable)\n- SHA-256 (default) or SHA-512 — opt-in per client/server pair\n- Built-in retry with exponential backoff (`SignedHttpClient`)\n- Edge Runtime support — Cloudflare Workers, Deno, Vercel Edge (`/edge`)\n- Zero hard runtime dependencies — peer deps are all optional\n- Dual ESM + CJS, full `.d.ts` types per subpath\n\n## Install\n\n```bash\nnpm install @daxmadov/hmac-kit\n```\n\nOptional, only if you use the matching feature:\n\n```bash\nnpm install ioredis           # for RedisNonceStore\nnpm install express           # for the Express adapter\nnpm install fastify           # for the Fastify adapter\nnpm install @nestjs/common    # for the NestJS adapter\n```\n\nRequires Node.js 18+.\n\n## Quick start\n\n### Client\n\n```ts\nimport { SignClient } from '@daxmadov/hmac-kit/client';\n\nconst signer = new SignClient({\n  clientId: 'svc_a',\n  secret: process.env.API_SECRET!,\n});\n\nconst body = JSON.stringify({ amount: 100 });\nconst { headerName, headerValue } = signer.sign({\n  method: 'POST',\n  path: '/api/payments',\n  body, // EXACT bytes you'll send on the wire\n});\n\nawait fetch('https://api.example.com/api/payments', {\n  method: 'POST',\n  headers: {\n    'Content-Type': 'application/json',\n    [headerName]: headerValue,\n  },\n  body, // <-- same string you signed\n});\n```\n\nOr, with the built-in fetch wrapper:\n\n```ts\nimport { SignedHttpClient } from '@daxmadov/hmac-kit/client';\n\nconst client = new SignedHttpClient({\n  baseUrl: 'https://api.example.com',\n  clientId: 'svc_a',\n  secret: process.env.API_SECRET!,\n});\n\nconst res = await client.post('/api/payments', { amount: 100 });\n```\n\n#### Retry on transient errors\n\n`SignedHttpClient` can retry automatically. Each retry re-signs with a\nfresh nonce and timestamp, so replay protection is never compromised.\n\n```ts\nconst client = new SignedHttpClient({\n  baseUrl: 'https://api.example.com',\n  clientId: 'svc_a',\n  secret: process.env.API_SECRET!,\n  retry: {\n    attempts: 3,           // total attempts including the first (default: 1)\n    delayMs: 500,          // base delay in ms (default: 500)\n    backoff: 'exponential',// 'exponential' or 'fixed' (default: 'exponential')\n    statusCodes: [429, 500, 502, 503, 504], // which codes to retry\n  },\n});\n```\n\n#### SHA-512\n\nPass `signatureAlgorithm: 'sha512'` to both client and server to use\nHMAC-SHA-512. Both sides must use the same algorithm.\n\n```ts\nconst signer = new SignClient({\n  clientId: 'svc_a',\n  secret: process.env.API_SECRET!,\n  signatureAlgorithm: 'sha512',\n});\n\nconst verifier = new SignatureVerifier({\n  getSecret: async (id) => db.getSecret(id),\n  signatureAlgorithm: 'sha512',\n});\n```\n\n### Server (raw)\n\n```ts\nimport {\n  SignatureVerifier,\n  MemoryNonceStore,\n  HmacAuthError,\n} from '@daxmadov/hmac-kit/server';\n\nconst verifier = new SignatureVerifier({\n  getSecret: async (clientId) => {\n    const row = await db.clients.findOne({ id: clientId });\n    return row?.secret ?? null;\n  },\n  nonceStore: new MemoryNonceStore(), // or RedisNonceStore for prod\n  timestampWindowSeconds: 300,\n});\n\ntry {\n  const { clientId } = await verifier.verify({\n    authHeader: req.headers['x-signature'],\n    method: req.method,\n    path: req.path,\n    rawBody: req.rawBody, // raw bytes — see \"Raw body\" below\n  });\n  // request is authentic; clientId is verified\n} catch (err) {\n  if (err instanceof HmacAuthError) {\n    res.status(err.httpStatus).json(err.toJSON());\n    return;\n  }\n  throw err;\n}\n```\n\n### Express adapter\n\n```ts\nimport express from 'express';\nimport { SignatureVerifier, MemoryNonceStore } from '@daxmadov/hmac-kit/server';\nimport {\n  createHmacMiddleware,\n  rawBodySaver,\n} from '@daxmadov/hmac-kit/adapters/express';\n\nconst verifier = new SignatureVerifier({\n  getSecret: async (id) => process.env.SHARED_SECRET ?? null,\n  nonceStore: new MemoryNonceStore(),\n});\n\nconst app = express();\napp.use(express.json({ verify: rawBodySaver })); // captures req.rawBody\napp.use(createHmacMiddleware(verifier));\n\napp.post('/api/payments', (req, res) => {\n  res.json({ ok: true, clientId: req.hmac!.clientId });\n});\n```\n\n### Fastify adapter\n\n```ts\nimport Fastify from 'fastify';\nimport { SignatureVerifier, MemoryNonceStore } from '@daxmadov/hmac-kit/server';\nimport { hmacAuthPlugin } from '@daxmadov/hmac-kit/adapters/fastify';\n\nconst verifier = new SignatureVerifier({ /* ... */ });\nconst app = Fastify();\nawait app.register(hmacAuthPlugin, { verifier });\n\napp.post('/api/payments', async (req) => ({ clientId: req.hmac?.clientId }));\n```\n\n### NestJS adapter\n\nEnable raw body capture at app boot:\n\n```ts\nconst app = await NestFactory.create(AppModule, { rawBody: true });\n```\n\nWire the guard:\n\n```ts\nimport { Module, UseGuards, Post, Req } from '@nestjs/common';\nimport { SignatureVerifier, MemoryNonceStore } from '@daxmadov/hmac-kit/server';\nimport { HmacAuthGuard, HMAC_VERIFIER } from '@daxmadov/hmac-kit/adapters/nestjs';\n\n@Module({\n  providers: [\n    {\n      provide: HMAC_VERIFIER,\n      useFactory: () =>\n        new SignatureVerifier({\n          getSecret: async () => process.env.SHARED_SECRET ?? null,\n          nonceStore: new MemoryNonceStore(),\n        }),\n    },\n    {\n      provide: HmacAuthGuard,\n      useFactory: (v: SignatureVerifier) => new HmacAuthGuard(v),\n      inject: [HMAC_VERIFIER],\n    },\n  ],\n  exports: [HmacAuthGuard],\n})\nexport class HmacModule {}\n\n@Controller('api')\nexport class PaymentsController {\n  @UseGuards(HmacAuthGuard)\n  @Post('payments')\n  handle(@Req() req: any) {\n    return { clientId: req.hmac.clientId };\n  }\n}\n```\n\n## Edge Runtime\n\nUse the `/edge` subpath for environments that do not support `node:crypto`\n(Cloudflare Workers, Deno, Vercel Edge Functions, Bun).\n\nThe Edge API is identical to the Node.js API except that `EdgeSignClient.sign()`\nis `async`.\n\n```ts\nimport {\n  EdgeSignClient,\n  EdgeSignatureVerifier,\n  EdgeSignedHttpClient,\n  MemoryNonceStore,\n} from '@daxmadov/hmac-kit/edge';\n\n// Client\nconst signer = new EdgeSignClient({\n  clientId: 'svc_a',\n  secret: process.env.API_SECRET!,\n});\n\nconst { headerName, headerValue } = await signer.sign({\n  method: 'POST',\n  path: '/api/payments',\n  body: JSON.stringify({ amount: 100 }),\n});\n\n// Server (e.g. Cloudflare Worker)\nconst verifier = new EdgeSignatureVerifier({\n  getSecret: async (id) => env.SECRETS[id] ?? null,\n  nonceStore: new MemoryNonceStore(),\n});\n\nexport default {\n  async fetch(request: Request, env: Env): Promise<Response> {\n    const url = new URL(request.url);\n    const rawBody = await request.text();\n\n    try {\n      const { clientId } = await verifier.verify({\n        authHeader: request.headers.get('x-signature'),\n        method: request.method,\n        path: url.pathname + url.search,\n        rawBody,\n      });\n      return Response.json({ ok: true, clientId });\n    } catch (err: any) {\n      return Response.json(err.toJSON(), { status: err.httpStatus ?? 500 });\n    }\n  },\n};\n```\n\nThe `EdgeSignedHttpClient` has the same retry support as `SignedHttpClient`.\n\n> **Node.js**: You can also use the `/edge` export in Node.js 18+ — both\n> `node:crypto` and `globalThis.crypto` are available there.\n\n## Raw body\n\nThe verifier hashes the **exact bytes** the client signed. If your HTTP\nframework parses the body before the verifier sees it (Express, NestJS,\nFastify), you must capture the raw bytes:\n\n- **Express**: use the included `rawBodySaver` as `express.json({ verify })`.\n- **Fastify**: the included plugin registers a content-type parser that\n  populates `req.rawBody` automatically.\n- **NestJS**: pass `{ rawBody: true }` to `NestFactory.create`.\n\nRe-stringifying parsed JSON will silently change bytes (key order,\nwhitespace, escaping) and the body-hash check will fail.\n\n## Protocol\n\nThe string-to-sign is the following five fields joined by `\\n`:\n\n```\n<METHOD>\\n\n<path>\\n\n<unix-seconds>\\n\n<nonce-uuid-v4>\\n\n<sha256-hex(body)>   ← sha512-hex when signatureAlgorithm is 'sha512'\n```\n\nSignature = `HMAC-<algorithm>(secret, stringToSign)`, hex-encoded.\nDefault algorithm is SHA-256; opt into SHA-512 via `signatureAlgorithm: 'sha512'`\non both client and server.\n\nThe auth fields are JSON-encoded, base64-encoded, and transmitted as a\nsingle `X-Signature` header. The server decodes, validates timestamp and\nnonce, recomputes the signature, and constant-time compares.\n\nVerification order is deliberate: cheap checks first (header presence,\nformat, timestamp, nonce read), secret lookup AFTER timestamp and nonce,\nnonce stored ONLY after the signature verifies.\n\n## Errors\n\nAll verification errors extend `HmacAuthError`. Each has a stable `code`\nstring and a recommended `httpStatus`:\n\n| Class                   | code                | status |\n| ----------------------- | ------------------- | ------ |\n| `MissingHeaderError`    | `MISSING_HEADER`    | 401    |\n| `InvalidFormatError`    | `INVALID_FORMAT`    | 400    |\n| `ExpiredRequestError`   | `EXPIRED_REQUEST`   | 401    |\n| `ReplayAttackError`     | `REPLAY_ATTACK`     | 401    |\n| `UnknownClientError`    | `UNKNOWN_CLIENT`    | 401    |\n| `InvalidSignatureError` | `INVALID_SIGNATURE` | 401    |\n| `BodyHashMismatchError` | `BODY_HASH_MISMATCH`| 400    |\n| `InternalAuthError`     | `INTERNAL_ERROR`    | 500    |\n\nUse `err.toJSON()` to get a safe-to-send error body — secrets and raw\nsignatures never appear in messages.\n\n## Security checklist\n\n- Constant-time signature comparison (`crypto.timingSafeEqual` with strict\n  pre-validation).\n- Hex/base64 length and format check before `timingSafeEqual` to avoid\n  exception-based length oracles.\n- Verification order: cheap checks first, secret lookup AFTER timestamp +\n  nonce, nonce stored ONLY after signature verifies.\n- Nonces keyed per client (`clientId:nonce`) to prevent cross-client\n  collisions.\n- Secrets never appear in error messages, never in `toString` / `toJSON`.\n- Cryptographically-strong nonces (`crypto.randomUUID`).\n- Verifier never parses or re-serializes the request body — raw bytes only.\n\n## Build & develop\n\n```bash\nnpm install\nnpm run typecheck\nnpm run lint\nnpm test\nnpm run build      # produces dist/ (ESM + CJS + .d.ts per entry)\n```\n\n## License\n\nMIT — see `LICENSE`.\n","readmeFilename":"README.md"}