Files
portal/src/lib/server/v2-signature.spec.ts
T
NeilClack 91d162c746 Rebuild admin pages around DataTable/AdminModal; restructure categories/groups and v2 groups API
Remove the generic $lib/svelte-admin CRUD layer and AdminTable.svelte; each
admin route now owns its own form/modal logic. Add DataTable.svelte,
superform-errors.ts, and profile-specific modal/field components.

Flatten proclamation categories/groups and drop subscription levels; add the
v2 groups API; update v1/v2 endpoints, serializers, auth, and regenerated
Prisma client with the new migrations.

Document the overhaul plan in docs/ADMIN_OVERHAUL.md.
2026-09-10 09:38:01 -05:00

121 lines
3.7 KiB
TypeScript

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<Record<'signature' | 'nonce' | 'timestamp' | 'body', string>>;
}) {
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');
});
});