Files
HealthLog/src/lib/random-id.ts
T
Marc-André Bombeck 5556478691 Make uploads and every other write work over plain HTTP
crypto.randomUUID() is [SecureContext] in the Web Cryptography IDL, so a
page served over plain HTTP never gets it. Measured in Chromium against
http://<lan-ip>: isSecureContext false, randomUUID and subtle undefined,
getRandomValues still a function.

The document vault minted its queue id and Idempotency-Key that way, inside
the file-input change handler. On a LAN self-host the call threw before a
queue row existed, so picking a file produced no upload, no error and no
trace — the picker simply looked inert, and the only way through was to
reach the app over HTTPS. The client transport has minted a default
Idempotency-Key the same way since v1.37.18, which put every apiPost and
apiPatch on the same footing: on such a host nothing wrote at all.

Add randomId(), which prefers the platform call and otherwise assembles a
v4 UUID from getRandomValues — the same CSPRNG, the same 122 bits, no
secure context required — and route every browser-side call site through
it. Nothing in an upload needs one: the bytes travel over multipart XHR and
every cryptographic operation happens on the server. Passkeys, web push and
home-screen install still need HTTPS; browsers withhold those outright.

A structural guard fails a direct crypto.randomUUID() anywhere outside the
two server-side files that may keep it, so the class cannot return one call
site at a time.

Also point the CSV and JSON import cards straight at the import guide. They
resolved through a redirect under the integrations tree, which is the wrong
place for a guide that is not an integration runbook. The suite that
covered those links pinned the old spelling rather than the guarantee; it
now asserts an absolute docs-site URL, no internal path, and new-tab
hygiene.
2026-09-05 16:22:39 +02:00

56 lines
2.2 KiB
TypeScript

/**
* A random v4 UUID that also works on a plain-HTTP origin.
*
* `crypto.randomUUID()` is marked `[SecureContext]` in the Web Cryptography
* IDL, so on a LAN self-host served over plain HTTP the property is simply
* absent and the call throws `TypeError: crypto.randomUUID is not a
* function`. Measured in Chromium on `http://<lan-ip>`: `isSecureContext`
* false, `crypto.randomUUID` and `crypto.subtle` undefined — but
* `crypto.getRandomValues` still a function, because it carries no
* `[SecureContext]` marker.
*
* That threw where nobody caught it. The document-vault upload minted a
* queue id and an `Idempotency-Key` this way inside the file-input change
* handler, so picking a file on a plain-HTTP host did nothing at all: no
* queue row, no request, no error. Since the client transport started
* minting a default `Idempotency-Key`, every `apiPost` / `apiPatch` in the
* app failed the same way on such a host.
*
* So the id is built from `getRandomValues` whenever `randomUUID` is out of
* reach. Same 122 bits of entropy from the same CSPRNG, same RFC 4122
* version-4 layout, no secure context required. Nothing in an upload needs
* one: the bytes go out over multipart XHR and every cryptographic
* operation happens on the server.
*/
/** Hex lookup for the byte→string step; built once. */
const HEX = Array.from({ length: 256 }, (_, i) =>
i.toString(16).padStart(2, "0"),
);
/**
* A fresh RFC 4122 version-4 UUID.
*
* Prefers the platform's own `randomUUID` and falls back to the
* always-available `getRandomValues`. Use this instead of
* `crypto.randomUUID()` in anything that runs in the browser — the
* `client-random-id-guard` test fails a direct call.
*/
export function randomId(): string {
if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
const bytes = crypto.getRandomValues(new Uint8Array(16));
// RFC 4122 §4.4: version nibble to 4, variant bits to 0b10.
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = Array.from(bytes, (b) => HEX[b]!).join("");
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20),
].join("-");
}