Преглед изворни кода

fix(connection): mint RPC ids on insecure origins

imccyu пре 2 месеци
родитељ
комит
ede278d0c7

+ 2 - 1
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<P>(payload: P): RpcRequest<P> {
-  return { rpcId: RpcId(crypto.randomUUID()), payload }
+  return { rpcId: RpcId(randomUuid()), payload }
 }
 
 function text(t: string): ContentBlock[] {

+ 14 - 0
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)}`
+}

+ 2 - 1
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,

+ 8 - 1
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' } },
     })