All files / lib share-token.ts

100% Statements 16/16
100% Branches 6/6
100% Functions 2/2
100% Lines 14/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 338x         8x 8x 8x           8x     1011x 1008x       1008x       1004x 1004x 1004x 1004x 16064x 1004x    
const ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
// 8-char suffix from a 36-char alphabet was ~41 bits, weak for a permanent
// bearer token that grants board read-access AND auto-enrolls the caller as
// a co-editor. 16 chars gives ~82 bits, well above the 80-bit cryptographic
// minimum for a bearer secret.
const RANDOM_LEN = 16;
const MAX_PREFIX_LEN = 20;
const FALLBACK_PREFIX = "user";
 
// Accept both legacy 8-char suffix and new 16-char suffix so existing share
// URLs in the wild keep resolving. New generateShareToken() always emits
// 16 chars, so over time the legacy surface shrinks naturally as owners
// revoke and re-share. A future PR can tighten to {16} only.
export const SHARE_TOKEN_REGEX = /^[a-z0-9]{1,20}-[a-z0-9]{8}(?:[a-z0-9]{8})?$/;
 
export function sanitizeUserPrefix(raw: string | null | undefined): string {
	if (!raw || typeof raw !== "string") return FALLBACK_PREFIX;
	const cleaned = raw
		.toLowerCase()
		.replace(/[^a-z0-9]/g, "")
		.slice(0, MAX_PREFIX_LEN);
	return cleaned.length > 0 ? cleaned : FALLBACK_PREFIX;
}
 
export function generateShareToken(userName: string | null | undefined): string {
	const prefix = sanitizeUserPrefix(userName);
	const bytes = new Uint8Array(RANDOM_LEN);
	crypto.getRandomValues(bytes);
	let suffix = "";
	for (const b of bytes) suffix += ALPHABET[b % ALPHABET.length];
	return `${prefix}-${suffix}`;
}