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:
2026-04-14 02:41:39 +02:00
parent ab8ac08226
commit 53de447aa5
12 changed files with 181 additions and 15 deletions

View File

@@ -32,6 +32,7 @@
import { scheduleUndoable } from '$lib/sync/undoQueue';
import SyncBanner from '$lib/components/SyncBanner.svelte';
import { getSupabase } from '$lib/supabase';
import { generateId } from '$lib/utils/id';
import type { ShoppingItem, ShoppingList } from '@colectivo/types';
import ItemSuggestions from '$lib/components/ItemSuggestions.svelte';
import {
@@ -306,7 +307,7 @@
if (!name || !$currentUser) return;
const sortOrder = items.length;
const tempId = crypto.randomUUID();
const tempId = generateId();
const optimistic: ShoppingItem = {
id: tempId,
list_id: listId,

View File

@@ -16,6 +16,7 @@
renameTaskList
} from '$lib/stores/tasks';
import { getSupabase } from '$lib/supabase';
import { generateId } from '$lib/utils/id';
import type { Task, TaskList } from '@colectivo/types';
import { ArrowLeft, GripVertical, Plus, Trash2, Check } from 'lucide-svelte';
import * as m from '$lib/paraglide/messages';
@@ -134,7 +135,7 @@
const title = newTitle.trim();
if (!title || !$currentUser) return;
const sortOrder = pendingTasks.length;
const tempId = crypto.randomUUID();
const tempId = generateId();
const optimistic: Task = {
id: tempId,
list_id: listId,