diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 31747bb311..e13c0a19f6 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -35,10 +35,11 @@ import type { } from './api.ts' import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api' import { AbstractApiClient, RpcId, SESSION_SEARCH_RESULT_LIMIT } from './api.ts' +import { randomUuid } from './random-uuid.ts' /** The fake carrier mints like a real one (business code never mints). */ function rpcRequest
(payload: P): RpcRequest
{ - return { rpcId: RpcId(crypto.randomUUID()), payload } + return { rpcId: RpcId(randomUuid()), payload } } function text(t: string): ContentBlock[] { diff --git a/packages/client/connection/src/client/random-uuid.ts b/packages/client/connection/src/client/random-uuid.ts new file mode 100644 index 0000000000..dc3106bd86 --- /dev/null +++ b/packages/client/connection/src/client/random-uuid.ts @@ -0,0 +1,14 @@ +/** Browser-safe UUID generation for client-side wire correlation. */ + +/** + * Generate an RFC 4122 version 4 UUID without requiring a secure context. + * @returns a UUID backed by `crypto.getRandomValues()`, which browsers expose on insecure origins. + */ +export function randomUuid(): string { + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)) + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) + view.setUint8(6, (view.getUint8(6) & 0x0f) | 0x40) + view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80) + const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('') + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` +} diff --git a/packages/client/connection/src/client/rpc.ts b/packages/client/connection/src/client/rpc.ts index 0c12149d7b..7883f2a9d3 100644 --- a/packages/client/connection/src/client/rpc.ts +++ b/packages/client/connection/src/client/rpc.ts @@ -6,6 +6,7 @@ import { type ClientRequest, } from '@deepseek-ai/dsh-host-apiproxy/api' import type { ClientConnectionRpc } from '../rpc.ts' +import { randomUuid } from './random-uuid.ts' const INTERNAL_BASE = 'http://dsh.internal' const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/ @@ -19,7 +20,7 @@ export function createWebConnectionRpc(): ClientConnectionRpc { return { async call(channel, endpoint, payload, signal) { assertTarget(channel, endpoint) - const rpcId = RpcId(crypto.randomUUID()) + const rpcId = RpcId(randomUuid()) const message: ClientRequest = { type: 'client-request', rpcId, diff --git a/packages/client/connection/tests/client-apply.spec.ts b/packages/client/connection/tests/client-apply.spec.ts index 6bf9c26b46..41e8e9b0e2 100644 --- a/packages/client/connection/tests/client-apply.spec.ts +++ b/packages/client/connection/tests/client-apply.spec.ts @@ -204,8 +204,13 @@ describe('connection client apply', () => { expect(sockets[0]?.readyState).toBe(FakeWebSocket.CLOSED) }) - it('carries RPC calls over the shared API channel with rpcId echo validation', async () => { + it('carries RPC calls without requiring secure-context randomUUID', async () => { ;(globalThis as Win).location = { hostname: 'localhost', search: '' } + vi.stubGlobal('crypto', { + getRandomValues(bytes: Uint8Array) { + return bytes.fill(0) + }, + }) const handle = await mount() const original = globalThis.fetch const seen: { url: string; body: unknown }[] = [] @@ -225,11 +230,13 @@ describe('connection client apply', () => { .resolves.toEqual({ ok: true, value: { ref: 'goal-1' } }) } finally { globalThis.fetch = original + vi.unstubAllGlobals() } expect(seen).toHaveLength(1) expect(seen[0]?.url).toBe('http://dsh.internal/api/goals/create') expect(seen[0]?.body).toMatchObject({ type: 'client-request', + rpcId: '00000000-0000-4000-8000-000000000000', method: 'goals/create', payload: { args: { agentId: 'agent-1' } }, })