diff --git a/web/src/lib/createClientTurnId.test.ts b/web/src/lib/createClientTurnId.test.ts new file mode 100644 index 00000000..5d2e8b0a --- /dev/null +++ b/web/src/lib/createClientTurnId.test.ts @@ -0,0 +1,65 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { createClientTurnId, isUuidV4 } from './createClientTurnId'; + +const originalCrypto = globalThis.crypto; + +function restoreCrypto() { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: originalCrypto, + }); +} + +test('createClientTurnId uses randomUUID when available', () => { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + randomUUID: () => '11111111-1111-4111-8111-111111111111', + }, + }); + + try { + assert.equal(createClientTurnId(), '11111111-1111-4111-8111-111111111111'); + } finally { + restoreCrypto(); + } +}); + +test('createClientTurnId falls back to getRandomValues UUID v4', () => { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: { + getRandomValues: (bytes: Uint8Array) => { + for (let i = 0; i < bytes.length; i += 1) { + bytes[i] = i; + } + return bytes; + }, + }, + }); + + try { + const id = createClientTurnId(); + assert.equal(isUuidV4(id), true); + assert.match(id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i); + } finally { + restoreCrypto(); + } +}); + +test('createClientTurnId throws when no secure random API exists', () => { + Object.defineProperty(globalThis, 'crypto', { + configurable: true, + value: {}, + }); + + try { + assert.throws( + () => createClientTurnId(), + /当前浏览器不支持安全随机标识/ + ); + } finally { + restoreCrypto(); + } +}); diff --git a/web/src/lib/createClientTurnId.ts b/web/src/lib/createClientTurnId.ts new file mode 100644 index 00000000..d719e39d --- /dev/null +++ b/web/src/lib/createClientTurnId.ts @@ -0,0 +1,39 @@ +const UUID_V4_REGEX = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +function formatUuidV4(bytes: Uint8Array): string { + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, '0')); + + return [ + hex.slice(0, 4).join(''), + hex.slice(4, 6).join(''), + hex.slice(6, 8).join(''), + hex.slice(8, 10).join(''), + hex.slice(10, 16).join(''), + ].join('-'); +} + +/** + * 生成客户端 turn 标识,兼容非 Secure Context(如局域网 HTTP)。 + * 优先 randomUUID;否则用 getRandomValues 生成 RFC 4122 v4 UUID。 + */ +export function createClientTurnId(): string { + if (typeof globalThis.crypto?.randomUUID === 'function') { + return globalThis.crypto.randomUUID(); + } + + if (typeof globalThis.crypto?.getRandomValues === 'function') { + const bytes = new Uint8Array(16); + globalThis.crypto.getRandomValues(bytes); + return formatUuidV4(bytes); + } + + throw new Error('当前浏览器不支持安全随机标识,请升级浏览器或通过 HTTPS 访问'); +} + +export function isUuidV4(value: string): boolean { + return UUID_V4_REGEX.test(value); +} diff --git a/web/src/types/sendMessage.ts b/web/src/types/sendMessage.ts new file mode 100644 index 00000000..86e44e09 --- /dev/null +++ b/web/src/types/sendMessage.ts @@ -0,0 +1,4 @@ +export type SendMessageResult = { + accepted: boolean; + error?: string; +};