The token is the entire access control on an endpoint the whole internet can reach, so both halves are pure and tested directly rather than through a request — the same reasoning that has uploadTypes.ts and keyByCallerAndEmail exported for their tests. 32 bytes of CSPRNG output, base64url so the value survives being pasted into a URL, a chat message or a QR code without escaping. That matters for something a person is handed rather than something a machine reads. The collision test runs a thousand generations rather than asserting the obvious, because a repeat would mean one person's link opening another's. SHA-256 rather than bcrypt, and the reasoning inverts the one that governs passwords. A password hash is slow on purpose because a human password carries little entropy and must survive an offline dictionary attack. This is 256 bits from a CSPRNG: there is no dictionary, so slowing the hash buys nothing. Meanwhile the digest is computed on every submission to an unauthenticated endpoint, where a deliberately slow hash would be a denial-of-service surface — #242 is the local proof that cost-12 hashing on a request path is enough to push it past a timeout under load. No timing-safe comparison, deliberately: the lookup is an indexed equality match on the digest rather than a byte-by-byte compare of the secret, and an attacker who could mount a timing attack against a 256-bit random value would still need the value. Backend: 307 unit tests, lint unchanged at 6 pre-existing warnings, build clean. Ref #222
32 lines
1.0 KiB
TypeScript
32 lines
1.0 KiB
TypeScript
import { generateToken, hashToken } from '../../src/uploadLinks';
|
|
|
|
describe('generateToken', () => {
|
|
it('produces a URL-safe token with no padding', () => {
|
|
expect(generateToken()).toMatch(/^[A-Za-z0-9_-]{43}$/);
|
|
});
|
|
|
|
// The token is the entire access control on the intake endpoint. If two
|
|
// calls could collide, one person's link would open another's.
|
|
it('does not repeat', () => {
|
|
const seen = new Set(Array.from({ length: 1000 }, () => generateToken()));
|
|
expect(seen.size).toBe(1000);
|
|
});
|
|
});
|
|
|
|
describe('hashToken', () => {
|
|
it('is a lowercase hex sha256 digest', () => {
|
|
expect(hashToken('abc')).toBe(
|
|
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
|
|
);
|
|
});
|
|
|
|
it('is stable across calls, so a stored digest keeps matching', () => {
|
|
const token = generateToken();
|
|
expect(hashToken(token)).toBe(hashToken(token));
|
|
});
|
|
|
|
it('gives different tokens different digests', () => {
|
|
expect(hashToken(generateToken())).not.toBe(hashToken(generateToken()));
|
|
});
|
|
});
|