Problem The phone-preview path (http://192.168.1.167:5173) is a non-secure origin. `window.crypto.randomUUID` is gated behind secure contexts (HTTPS / localhost / 127.0.0.1 / file://) so it's undefined there, and every optimistic-id call site crashed handleAdd with `TypeError: crypto.randomUUID is not a function`. Fix apps/web/src/lib/utils/id.ts — generateId() uses crypto.randomUUID() when available, otherwise falls back to crypto.getRandomValues() + RFC 4122 §4.4 byte assembly. getRandomValues IS exposed on insecure contexts (unlike subtle), so the fallback is v4-compliant and cryptographically acceptable for client-side optimistic ids. Replaced crypto.randomUUID() at every call site: stores/lists.ts, stores/tasks.ts, sync/queue.ts, sync/undoQueue.ts routes/(app)/lists/[id]/+page.svelte routes/(app)/tasks/[id]/+page.svelte CLAUDE.md gotcha added: never call crypto.randomUUID() directly — always import generateId from $lib/utils/id. Tests (written first, confirmed failing on main) src/lib/utils/id.test.ts (4 tests) U-01 returns UUID v4 when randomUUID is available U-02 falls back when randomUUID is undefined U-03 50 fallback-generated ids are all unique U-04 prefers randomUUID when present (doesn't drop into fallback) tests/e2e/insecure-context.test.ts (1 test) INS-01 stubs crypto.randomUUID = undefined via addInitScript, drives the add-item flow, confirms the row renders (if the old call site were still there, handleAdd would throw). Verification just test-all → exit 0, 233 green, 2 skipped: 34 pgTAP (unchanged) 140 Vitest integration + 2 skipped presence (unchanged) 15 Vitest unit (was 11, +4 generateId) 44 Playwright (was 43, +1 INS-01) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
50 lines
1.5 KiB
TypeScript
50 lines
1.5 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
import { generateId } from './id';
|
|
|
|
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
describe('generateId', () => {
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it('U-01: returns a UUID v4-shaped string when crypto.randomUUID is available', () => {
|
|
const id = generateId();
|
|
expect(id).toMatch(UUID_V4);
|
|
});
|
|
|
|
it('U-02: falls back when crypto.randomUUID is undefined (insecure-context case)', () => {
|
|
// Simulate an HTTP LAN origin: window.crypto exists but randomUUID is not exposed.
|
|
const baseCrypto = globalThis.crypto;
|
|
vi.stubGlobal('crypto', {
|
|
getRandomValues: baseCrypto.getRandomValues.bind(baseCrypto)
|
|
// no randomUUID
|
|
});
|
|
|
|
const id = generateId();
|
|
expect(id).toMatch(UUID_V4);
|
|
});
|
|
|
|
it('U-03: every call returns a unique value (uniqueness under fallback)', () => {
|
|
const baseCrypto = globalThis.crypto;
|
|
vi.stubGlobal('crypto', {
|
|
getRandomValues: baseCrypto.getRandomValues.bind(baseCrypto)
|
|
});
|
|
|
|
const ids = new Set(Array.from({ length: 50 }, generateId));
|
|
expect(ids.size).toBe(50);
|
|
});
|
|
|
|
it('U-04: prefers crypto.randomUUID when present (does not drop into fallback)', () => {
|
|
const spy = vi.fn(() => '11111111-1111-4111-8111-111111111111');
|
|
vi.stubGlobal('crypto', {
|
|
randomUUID: spy,
|
|
getRandomValues: globalThis.crypto.getRandomValues.bind(globalThis.crypto)
|
|
});
|
|
|
|
const id = generateId();
|
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
expect(id).toBe('11111111-1111-4111-8111-111111111111');
|
|
});
|
|
});
|