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>
63 lines
1.7 KiB
TypeScript
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('');
|