Files
platform/src/workspaces/helpers/passkeys.ts
T
pastilhasandClaude Opus 5 74b4c7908a fix the crash over http: crypto.randomUUID is secure-context only
Chat took the whole page down at the end of every turn with
`TypeError: crypto.randomUUID is not a function`.

`crypto.randomUUID()` is SECURE-CONTEXT ONLY — over plain http on anything that
is not localhost it is not defined at all. Officer is reached at
http://officer-dev:9000, which is neither, so all eighteen call sites in the
frontend were throwing. The stack shows why it was fatal rather than merely
broken: it was called inside a `useState` initialiser, so the throw happened
during render and unmounted the tree. The assistant message that has no id yet
is created at the end of a turn, which is exactly when it fired.

No TLS needed. `crypto.getRandomValues()` carries no such restriction — it is on
`Crypto`, not `SubtleCrypto`, and works in an insecure context. helpers/random-id
uses randomUUID when it exists and otherwise assembles a v4 from the same CSPRNG:
same 122 bits, same version and variant bits. Verified both paths produce a UUID
matching the v4 pattern, including with randomUUID deleted.

`crypto.subtle` is not used anywhere in the frontend, so randomUUID was the whole
of the problem. Audio recording is a different matter — getUserMedia genuinely
requires a secure context and cannot be polyfilled.

Nine files, eighteen call sites. The vendored hls.mjs is left alone.

Two of my own mistakes on the way, both caught by parsing rather than by reading:
the rewrite added an import of the helper TO the helper, and inserted another one
inside a multi-line import block — the same trap as the officerdb move earlier
tonight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 03:23:31 +00:00

63 lines
1.7 KiB
TypeScript

import { randomId } from 'helpers/random-id';
// Frontend
type PasskeyUser = {
id: string;
email: string;
firstName: string;
lastName: string;
};
export function arrayBufferToBase64(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
return window.btoa(binary);
}
export function getCredentialPayload(challenge: number[], user: PasskeyUser) {
return {
challenge: new Uint8Array(challenge),
rp: {
name: 'Pertento AI',
},
user: {
id: Uint8Array.from(atob(btoa(user.id)), (c) => c.charCodeAt(0)),
name: user.email,
displayName: `${user.firstName} ${user.lastName}`,
},
pubKeyCredParams: [
{ type: 'public-key' as const, alg: -7 },
{ type: 'public-key' as const, alg: -257 },
],
};
}
export function getSigninPayload(challenge: number[], credentialIds: string[]) {
return {
challenge: new Uint8Array(challenge),
allowCredentials: credentialIds.map((id) => {
const urlSafeId = id.replace(/-/g, '+').replace(/_/g, '/');
const decodedId = atob(urlSafeId);
return {
type: 'public-key' as const,
id: Uint8Array.from(decodedId, (c) => c.charCodeAt(0)),
};
}),
userVerification: 'preferred' as const,
};
}
// Backend
export function createChallenge() {
const challenge = randomId();
const hex = challenge.replace(/-/g, '');
const array: number[] = new Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
array[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return array;
}
export const hexToString = (array: number[]) => array.map((byte) => byte.toString(16).padStart(2, '0')).join('');