fix: generateId() fallback for insecure-context origins
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>
This commit is contained in:
64
apps/web/tests/e2e/insecure-context.test.ts
Normal file
64
apps/web/tests/e2e/insecure-context.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* INS-series — insecure-context regression
|
||||
*
|
||||
* When the app is served over a non-secure origin (http://<LAN-IP>, used for
|
||||
* on-device phone previews), `window.crypto.randomUUID` is undefined and every
|
||||
* optimistic-id generator that called it crashed `handleAdd`.
|
||||
*
|
||||
* We reproduce that condition by shimming `crypto.randomUUID` to `undefined`
|
||||
* via `addInitScript` on a page that otherwise runs on localhost (where
|
||||
* Playwright has a deterministic OAuth path). Same code path, just with the
|
||||
* API removed.
|
||||
*/
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { USERS } from '../fixtures/users.js';
|
||||
import { loginAs } from '../fixtures/login.js';
|
||||
|
||||
const SEED_LIST = '/lists/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
|
||||
|
||||
test.describe('Insecure-context resilience', () => {
|
||||
test('INS-01: adding an item works when crypto.randomUUID is undefined', async ({ context, page }) => {
|
||||
await context.addInitScript(() => {
|
||||
// Simulate an insecure origin: randomUUID is gated behind secure
|
||||
// contexts and therefore missing. getRandomValues stays available.
|
||||
Object.defineProperty(window.crypto, 'randomUUID', {
|
||||
configurable: true,
|
||||
value: undefined
|
||||
});
|
||||
});
|
||||
|
||||
// Re-assert the stub after navigation in case the context re-injects.
|
||||
page.on('framenavigated', async () => {
|
||||
await page
|
||||
.evaluate(() => {
|
||||
Object.defineProperty(window.crypto, 'randomUUID', {
|
||||
configurable: true,
|
||||
value: undefined
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
|
||||
await loginAs(page, USERS.borja);
|
||||
await page.goto(SEED_LIST);
|
||||
|
||||
// Sanity: the stub stuck — the page sees randomUUID as undefined.
|
||||
const stubbed = await page.evaluate(() => typeof window.crypto.randomUUID);
|
||||
expect(stubbed).toBe('undefined');
|
||||
|
||||
const input = page.getByPlaceholder(/add item|añadir producto/i);
|
||||
await expect(input).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const itemName = `INS-01-${Date.now()}`;
|
||||
await input.fill(itemName);
|
||||
await input.press('Enter');
|
||||
|
||||
// The row must render — i.e. generateId() returned a valid UUID and the
|
||||
// optimistic insert + server round-trip completed. If the old
|
||||
// crypto.randomUUID() call site is still there, handleAdd throws a
|
||||
// TypeError and the row never appears.
|
||||
await expect(
|
||||
page.locator('[role="listitem"]').filter({ hasText: itemName }).first()
|
||||
).toBeVisible({ timeout: 5_000 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user