Skip to content

Commit 30c00dc

Browse files
ymansurozerclaude
andcommitted
fix: fall back to getRandomValues for UUIDs on insecure origins
crypto.randomUUID() is secure-context-gated, so it's undefined on plain-HTTP non-localhost desks (GALLEY_HOST/tailnet), and creating a comment threw. Add a uuid() helper that uses the native call when present and otherwise builds an RFC-4122 v4 UUID from crypto.getRandomValues(), which has no such gate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e8d9b5e commit 30c00dc

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

src/ui/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
import { setBaseTitle, reviewStats } from "./progress";
3838
import { installKeys, helpGroups, confirmYes, confirmNo } from "./keys";
3939
import { cursorReset, cursorSelection, invalidateCursorRows } from "./cursor";
40+
import { uuid } from "./uuid";
4041
import type { ReviewState, FileRow, Settings, DiffStyle } from "./types";
4142

4243
// Close the inline composer when clicking outside it (unless it has unsaved text). The
@@ -405,7 +406,7 @@ const submitComment = (intent: "question" | "action") => {
405406
const contents =
406407
cur.path === file.path ? (side === "deletions" ? cur.oldContents : cur.newContents) : undefined;
407408
const c = {
408-
id: crypto.randomUUID(),
409+
id: uuid(),
409410
path: file.path,
410411
side,
411412
lineNumber,

src/ui/uuid.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import { uuid, uuidFallback } from "./uuid";
4+
5+
// Standard v4 shape: 8-4-4-4-12 hex groups, version nibble "4", variant nibble in [89ab].
6+
const V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
7+
8+
test("uuidFallback returns v4-format ids", () => {
9+
assert.match(uuidFallback(), V4);
10+
});
11+
12+
test("uuidFallback: successive calls differ", () => {
13+
assert.notEqual(uuidFallback(), uuidFallback());
14+
});
15+
16+
test("uuid: falls back to v4 format when crypto.randomUUID is absent", () => {
17+
const original = crypto.randomUUID;
18+
// @ts-expect-error — simulating an insecure context, where randomUUID is undefined.
19+
crypto.randomUUID = undefined;
20+
try {
21+
assert.match(uuid(), V4);
22+
} finally {
23+
crypto.randomUUID = original;
24+
}
25+
});
26+
27+
test("uuid: uses the native implementation when available", () => {
28+
const original = crypto.randomUUID;
29+
const sentinel =
30+
"11111111-1111-4111-8111-111111111111" as `${string}-${string}-${string}-${string}-${string}`;
31+
crypto.randomUUID = () => sentinel;
32+
try {
33+
assert.equal(uuid(), sentinel);
34+
} finally {
35+
crypto.randomUUID = original;
36+
}
37+
});

src/ui/uuid.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// crypto.randomUUID() is secure-context-gated: on a plain-HTTP non-localhost origin
2+
// (GALLEY_HOST/tailnet desks, e.g. http://devbox:41443/) it's undefined and comment creation
3+
// would throw. crypto.getRandomValues() has no such gate, so build an RFC-4122 v4 UUID from it
4+
// when the native call isn't available — same shape as native, no Math.random fallback.
5+
export function uuidFallback(): string {
6+
const bytes = crypto.getRandomValues(new Uint8Array(16));
7+
bytes[6] = (bytes[6]! & 0x0f) | 0x40; // version 4
8+
bytes[8] = (bytes[8]! & 0x3f) | 0x80; // variant 10xx
9+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
10+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
11+
}
12+
13+
export function uuid(): string {
14+
return typeof crypto.randomUUID === "function" ? crypto.randomUUID() : uuidFallback();
15+
}

0 commit comments

Comments
 (0)