Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -17,10 +18,6 @@ interface ItemWithId {
value: string;
}

function generateId(): string {
return crypto.randomUUID();
}

export function ArrayFieldEditor({
values,
onChange,
Expand All @@ -30,7 +27,7 @@ export function ArrayFieldEditor({
}: ArrayFieldEditorProps) {
// Track items with stable IDs
const [items, setItems] = useState<ItemWithId[]>(() =>
values.map((value) => ({ id: generateId(), value }))
values.map((value) => ({ id: generateUUID(), value }))
);

// Track if we're making an internal change to avoid sync loops
Expand All @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand All @@ -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()),
};
}

Expand Down Expand Up @@ -63,7 +60,7 @@ function FeatureCard({ feature, index, onChange, onRemove }: FeatureCardProps) {
onChange({
...feature,
file_locations: [...locations, ''],
_locationIds: [...locationIds, generateId()],
_locationIds: [...locationIds, generateUUID()],
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpecOutput['implementation_roadmap']>[number];
type PhaseStatus = 'completed' | 'in_progress' | 'pending';
Expand All @@ -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 {
Expand Down
36 changes: 12 additions & 24 deletions apps/ui/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of generateUUID directly calls crypto.getRandomValues() without checking if the crypto object or its getRandomValues method is available. While the PR description states it works in modern browsers, it's a good defensive programming practice to include a check to prevent runtime errors in environments where crypto might be undefined or getRandomValues might be missing. Given the emphasis on cryptographic security, throwing an error is more appropriate than falling back to an insecure method if the required API is absent.

Suggested change
const bytes = new Uint8Array(16);
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)}`;
}