import { describe, it, expect, beforeEach } from 'vitest'; import { computeSignature, buildCanonicalRequest, verifyRequestSignature, _clearNonces } from './v2-signature'; const SECRET = 'test-onboarding-secret'; function signedRequest(opts: { now?: Date; timestampOffsetSec?: number; tamper?: Partial>; }) { const method = 'POST'; const path = '/api/v2/automations/create/account'; const timestamp = String(Math.floor((opts.now ?? new Date()).getTime() / 1000)); const nonce = 'a48c0f2e-5f34-4b0b-9a12-3c7d6b8e1f00'; const body = '{"email":"user@example.com"}'; let ts = timestamp; let nonceValue = nonce; if (opts.tamper?.timestamp) ts = opts.tamper.timestamp; if (opts.tamper?.nonce) nonceValue = opts.tamper.nonce; let sig = computeSignature(SECRET, buildCanonicalRequest(method, path, ts, nonceValue, body)); if (opts.tamper?.body) sig = computeSignature( SECRET, buildCanonicalRequest(method, path, ts, nonceValue, opts.tamper.body) ); if (opts.tamper?.signature) sig = opts.tamper.signature; const headers = new Headers({ 'content-type': 'application/json', 'x-timestamp': ts, 'x-nonce': nonceValue, 'x-signature': sig }); return { method, path, headers, rawBody: body }; } describe('verifyRequestSignature', () => { beforeEach(() => { _clearNonces(); }); it('accepts a correctly signed request', () => { const { method, path, headers, rawBody } = signedRequest({}); const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }); expect(result.ok).toBe(true); }); it('rejects a request with a wrong signature', () => { const { method, path, headers, rawBody } = signedRequest({ tamper: { signature: 'deadbeef'.repeat(8) } }); const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe('invalid signature'); }); it('rejects a request signed with the wrong secret', () => { const { method, path, headers, rawBody } = signedRequest({}); const result = verifyRequestSignature({ method, path, headers, rawBody, secret: 'other-secret' }); expect(result.ok).toBe(false); }); it('rejects missing signature headers', () => { const { method, path, rawBody } = signedRequest({}); const result = verifyRequestSignature({ method, path, headers: new Headers(), rawBody, secret: SECRET }); expect(result.ok).toBe(false); }); it('rejects an expired timestamp (>300s skew)', () => { const now = new Date(); const { method, path, headers, rawBody } = signedRequest({ tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 600) } }); const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now }); expect(result.ok).toBe(false); if (!result.ok) expect(result.reason).toBe('expired timestamp'); }); it('accepts a timestamp within the 300s skew', () => { const now = new Date(); const { method, path, headers, rawBody } = signedRequest({ tamper: { timestamp: String(Math.floor(now.getTime() / 1000) - 120) } }); const result = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET, now }); expect(result.ok).toBe(true); }); it('rejects a replayed nonce', () => { const { method, path, headers, rawBody } = signedRequest({}); expect(verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }).ok).toBe( true ); const replay = verifyRequestSignature({ method, path, headers, rawBody, secret: SECRET }); expect(replay.ok).toBe(false); if (!replay.ok) expect(replay.reason).toBe('replayed nonce'); }); });