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.
121 lines
3.7 KiB
TypeScript
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');
|
|
});
|
|
});
|