fix(connection): mint RPC ids on insecure origins

This commit is contained in:
imccyu
2026-08-07 01:16:07 +08:00
parent e8f2ab89bb
commit ede278d0c7
4 changed files with 26 additions and 3 deletions

View File

@@ -35,10 +35,11 @@ import type {
} from './api.ts' } from './api.ts'
import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api' import type { RequestPayload, ResponseValue, RpcMethodMap } from '@deepseek-ai/dsh-host-apiproxy/api'
import { AbstractApiClient, RpcId, SESSION_SEARCH_RESULT_LIMIT } from './api.ts' 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). */ /** The fake carrier mints like a real one (business code never mints). */
function rpcRequest<P>(payload: P): RpcRequest<P> { function rpcRequest<P>(payload: P): RpcRequest<P> {
return { rpcId: RpcId(crypto.randomUUID()), payload } return { rpcId: RpcId(randomUuid()), payload }
} }
function text(t: string): ContentBlock[] { function text(t: string): ContentBlock[] {

View File

@@ -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)}`
}

View File

@@ -6,6 +6,7 @@ import {
type ClientRequest, type ClientRequest,
} from '@deepseek-ai/dsh-host-apiproxy/api' } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { ClientConnectionRpc } from '../rpc.ts' import type { ClientConnectionRpc } from '../rpc.ts'
import { randomUuid } from './random-uuid.ts'
const INTERNAL_BASE = 'http://dsh.internal' const INTERNAL_BASE = 'http://dsh.internal'
const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/ const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/
@@ -19,7 +20,7 @@ export function createWebConnectionRpc(): ClientConnectionRpc {
return { return {
async call(channel, endpoint, payload, signal) { async call(channel, endpoint, payload, signal) {
assertTarget(channel, endpoint) assertTarget(channel, endpoint)
const rpcId = RpcId(crypto.randomUUID()) const rpcId = RpcId(randomUuid())
const message: ClientRequest = { const message: ClientRequest = {
type: 'client-request', type: 'client-request',
rpcId, rpcId,

View File

@@ -204,8 +204,13 @@ describe('connection client apply', () => {
expect(sockets[0]?.readyState).toBe(FakeWebSocket.CLOSED) 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: '' } ;(globalThis as Win).location = { hostname: 'localhost', search: '' }
vi.stubGlobal('crypto', {
getRandomValues(bytes: Uint8Array) {
return bytes.fill(0)
},
})
const handle = await mount() const handle = await mount()
const original = globalThis.fetch const original = globalThis.fetch
const seen: { url: string; body: unknown }[] = [] const seen: { url: string; body: unknown }[] = []
@@ -225,11 +230,13 @@ describe('connection client apply', () => {
.resolves.toEqual({ ok: true, value: { ref: 'goal-1' } }) .resolves.toEqual({ ok: true, value: { ref: 'goal-1' } })
} finally { } finally {
globalThis.fetch = original globalThis.fetch = original
vi.unstubAllGlobals()
} }
expect(seen).toHaveLength(1) expect(seen).toHaveLength(1)
expect(seen[0]?.url).toBe('http://dsh.internal/api/goals/create') expect(seen[0]?.url).toBe('http://dsh.internal/api/goals/create')
expect(seen[0]?.body).toMatchObject({ expect(seen[0]?.body).toMatchObject({
type: 'client-request', type: 'client-request',
rpcId: '00000000-0000-4000-8000-000000000000',
method: 'goals/create', method: 'goals/create',
payload: { args: { agentId: 'agent-1' } }, payload: { args: { agentId: 'agent-1' } },
}) })