diff --git a/apps/ui/src/components/views/spec-view/components/edit-mode/array-field-editor.tsx b/apps/ui/src/components/views/spec-view/components/edit-mode/array-field-editor.tsx index 773e691b2..bbaaa87c4 100644 --- a/apps/ui/src/components/views/spec-view/components/edit-mode/array-field-editor.tsx +++ b/apps/ui/src/components/views/spec-view/components/edit-mode/array-field-editor.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card } from '@/components/ui/card'; import { useRef, useState, useEffect } from 'react'; +import { generateUUID } from '@/lib/utils'; interface ArrayFieldEditorProps { values: string[]; @@ -17,10 +18,6 @@ interface ItemWithId { value: string; } -function generateId(): string { - return crypto.randomUUID(); -} - export function ArrayFieldEditor({ values, onChange, @@ -30,7 +27,7 @@ export function ArrayFieldEditor({ }: ArrayFieldEditorProps) { // Track items with stable IDs const [items, setItems] = useState(() => - values.map((value) => ({ id: generateId(), value })) + values.map((value) => ({ id: generateUUID(), value })) ); // Track if we're making an internal change to avoid sync loops @@ -44,11 +41,11 @@ export function ArrayFieldEditor({ } // External change - rebuild items with new IDs - setItems(values.map((value) => ({ id: generateId(), value }))); + setItems(values.map((value) => ({ id: generateUUID(), value }))); }, [values]); const handleAdd = () => { - const newItems = [...items, { id: generateId(), value: '' }]; + const newItems = [...items, { id: generateUUID(), value: '' }]; setItems(newItems); isInternalChange.current = true; onChange(newItems.map((item) => item.value)); diff --git a/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx b/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx index 1cdbac2f7..ad82a4d77 100644 --- a/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx +++ b/apps/ui/src/components/views/spec-view/components/edit-mode/features-section.tsx @@ -9,6 +9,7 @@ import { Badge } from '@/components/ui/badge'; import { ListChecks } from 'lucide-react'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; import type { SpecOutput } from '@automaker/spec-parser'; +import { generateUUID } from '@/lib/utils'; type Feature = SpecOutput['implemented_features'][number]; @@ -22,15 +23,11 @@ interface FeatureWithId extends Feature { _locationIds?: string[]; } -function generateId(): string { - return crypto.randomUUID(); -} - function featureToInternal(feature: Feature): FeatureWithId { return { ...feature, - _id: generateId(), - _locationIds: feature.file_locations?.map(() => generateId()), + _id: generateUUID(), + _locationIds: feature.file_locations?.map(() => generateUUID()), }; } @@ -63,7 +60,7 @@ function FeatureCard({ feature, index, onChange, onRemove }: FeatureCardProps) { onChange({ ...feature, file_locations: [...locations, ''], - _locationIds: [...locationIds, generateId()], + _locationIds: [...locationIds, generateUUID()], }); }; diff --git a/apps/ui/src/components/views/spec-view/components/edit-mode/roadmap-section.tsx b/apps/ui/src/components/views/spec-view/components/edit-mode/roadmap-section.tsx index 6275eebd8..b13f35e78 100644 --- a/apps/ui/src/components/views/spec-view/components/edit-mode/roadmap-section.tsx +++ b/apps/ui/src/components/views/spec-view/components/edit-mode/roadmap-section.tsx @@ -13,6 +13,7 @@ import { SelectValue, } from '@/components/ui/select'; import type { SpecOutput } from '@automaker/spec-parser'; +import { generateUUID } from '@/lib/utils'; type RoadmapPhase = NonNullable[number]; type PhaseStatus = 'completed' | 'in_progress' | 'pending'; @@ -21,12 +22,8 @@ interface PhaseWithId extends RoadmapPhase { _id: string; } -function generateId(): string { - return crypto.randomUUID(); -} - function phaseToInternal(phase: RoadmapPhase): PhaseWithId { - return { ...phase, _id: generateId() }; + return { ...phase, _id: generateUUID() }; } function internalToPhase(internal: PhaseWithId): RoadmapPhase { diff --git a/apps/ui/src/lib/utils.ts b/apps/ui/src/lib/utils.ts index 933ea1fd6..bdaaa9cff 100644 --- a/apps/ui/src/lib/utils.ts +++ b/apps/ui/src/lib/utils.ts @@ -156,35 +156,23 @@ export function sanitizeForTestId(name: string): string { /** * Generate a UUID v4 string. * - * Uses crypto.randomUUID() when available (secure contexts: HTTPS or localhost). - * Falls back to crypto.getRandomValues() for non-secure contexts (e.g., Docker via HTTP). + * Uses crypto.getRandomValues() which works in all modern browsers, + * including non-secure contexts (e.g., Docker via HTTP). * * @returns A RFC 4122 compliant UUID v4 string (e.g., "550e8400-e29b-41d4-a716-446655440000") */ export function generateUUID(): string { - // Use native randomUUID if available (secure contexts: HTTPS or localhost) - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID(); + if (typeof crypto === 'undefined' || typeof crypto.getRandomValues === 'undefined') { + throw new Error('Cryptographically secure random number generator not available.'); } + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); - // Fallback using crypto.getRandomValues() (works in all modern browsers, including non-secure contexts) - if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { - const bytes = new Uint8Array(16); - crypto.getRandomValues(bytes); + // Set version (4) and variant (RFC 4122) bits + bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122 - // Set version (4) and variant (RFC 4122) bits - bytes[6] = (bytes[6] & 0x0f) | 0x40; // Version 4 - bytes[8] = (bytes[8] & 0x3f) | 0x80; // Variant RFC 4122 - - // Convert to hex string with proper UUID format - const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; - } - - // Last resort fallback using Math.random() - less secure but ensures functionality - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); + // Convert to hex string with proper UUID format + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; }