mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +00:00
Add Orca Relay desktop and mobile transport (#8536)
* feat(mobile): define relay protocol groundwork Co-authored-by: Orca <help@stably.ai> * feat(mobile): implement replay-safe E2EE v2 sessions Co-authored-by: Orca <help@stably.ai> * test(auth): lock cloud refresh single-flight Co-authored-by: Orca <help@stably.ai> * test(mobile): complete E2EE v2 adversarial coverage Co-authored-by: Orca <help@stably.ai> * refactor(runtime): unify mobile socket wiring Co-authored-by: Orca <help@stably.ai> * feat(runtime): add relay control and data clients Co-authored-by: Orca <help@stably.ai> * feat(runtime): coordinate desktop relay sessions Co-authored-by: Orca <help@stably.ai> * fix(auth): fence stale cloud session mutations Co-authored-by: Orca <help@stably.ai> * feat(runtime): add relay pairing and durable revoke Co-authored-by: Orca <help@stably.ai> * feat(runtime): add relay credential pairing RPCs Co-authored-by: Orca <help@stably.ai> * feat(settings): show Orca Relay sign-in status Co-authored-by: Orca <help@stably.ai> * test(relay): prove desktop lifecycle and E2EE splice Co-authored-by: Orca <help@stably.ai> * feat(mobile): persist relay pairing state Co-authored-by: Orca <help@stably.ai> * feat(mobile): race direct and relay pairing Co-authored-by: Orca <help@stably.ai> * feat(mobile): recover pairing through relay director Co-authored-by: Orca <help@stably.ai> * fix(relay): preserve origin controls during drain Co-authored-by: Orca <help@stably.ai> * feat(mobile): recover interrupted relay pairing Co-authored-by: Orca <help@stably.ai> * feat(mobile): add stable relay RPC sessions Co-authored-by: Orca <help@stably.ai> * feat(mobile): supervise direct and relay endpoints Co-authored-by: Orca <help@stably.ai> * Cover mobile relay director fallback matrix Co-authored-by: Orca <help@stably.ai> * Fix relay settings component test isolation Co-authored-by: Orca <help@stably.ai> * Remove unrelated merge formatting drift Co-authored-by: Orca <help@stably.ai> * Update runtime connection count integration assertion Co-authored-by: Orca <help@stably.ai> * Run mobile typecheck through pnpm Co-authored-by: Orca <help@stably.ai> * feat(relay): gate desktop controls on mobile demand Co-authored-by: Orca <help@stably.ai> * test(mobile): cover served relay recovery Co-authored-by: Orca <help@stably.ai> * feat(mobile): upgrade direct pairings to relay Co-authored-by: Orca <help@stably.ai> * fix(relay): harden mobile reconnect and teardown Co-authored-by: Orca <help@stably.ai> * fix(auth): clarify account sign-in state Co-authored-by: Orca <help@stably.ai> * fix(auth): polish sign-in completion flow Co-authored-by: Orca <help@stably.ai> * fix(auth): clarify sign-out confirmation Co-authored-by: Orca <help@stably.ai> * fix(auth): simplify sign-in completion page Co-authored-by: Orca <help@stably.ai> * feat(mobile): add per-device pairing connection mode Co-authored-by: Orca <help@stably.ai> * fix(mobile): stabilize pairing option layout Co-authored-by: Orca <help@stably.ai> * fix(mobile): give pairing choices stable space Co-authored-by: Orca <help@stably.ai> * fix(mobile): stabilize pairing QR regeneration Co-authored-by: Orca <help@stably.ai> * Animate mobile pairing flow height Co-authored-by: Orca <help@stably.ai> * Configure auth in packaged builds Co-authored-by: Orca <help@stably.ai> * Make Orca Relay pairing an opt-in beta Co-authored-by: Orca <help@stably.ai> * Show Relay beta details on hover Co-authored-by: Orca <help@stably.ai> * Refine mobile relay pairing choice Co-authored-by: Orca <help@stably.ai> * Polish Orca Relay pairing controls Co-authored-by: Orca <help@stably.ai> * Keep mobile contract fallback test additive Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -34,6 +34,7 @@ import { SKILL_METHODS } from './skills'
|
||||
import { CLIPBOARD_METHODS } from './clipboard'
|
||||
import { HOST_CAPABILITY_METHODS } from './host-capabilities'
|
||||
import { EMULATOR_METHODS } from './emulator'
|
||||
import { PAIRING_METHODS } from './pairing'
|
||||
|
||||
// Why: a flat manifest keeps registration order explicit and provides one
|
||||
// grep-point for "what methods does the RPC server expose?" — useful when
|
||||
@@ -73,5 +74,6 @@ export const ALL_RPC_METHODS: readonly RpcAnyMethod[] = [
|
||||
...HOST_CAPABILITY_METHODS,
|
||||
...CLIENT_EVENT_METHODS,
|
||||
...CLIENT_UI_METHODS,
|
||||
...EMULATOR_METHODS
|
||||
...EMULATOR_METHODS,
|
||||
...PAIRING_METHODS
|
||||
]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { RpcDispatcher } from '../dispatcher'
|
||||
import { PAIRING_METHODS } from './pairing'
|
||||
|
||||
function dispatchPairing(
|
||||
method: string,
|
||||
params: unknown,
|
||||
pairing: NonNullable<Parameters<RpcDispatcher['dispatchStreaming']>[2]>['pairing']
|
||||
): Promise<Record<string, unknown>> {
|
||||
return new Promise((resolve) => {
|
||||
const dispatcher = new RpcDispatcher({
|
||||
runtime: new OrcaRuntimeService(),
|
||||
methods: PAIRING_METHODS
|
||||
})
|
||||
void dispatcher.dispatchStreaming(
|
||||
{ id: 'request-1', authToken: '', method, params },
|
||||
(response) => resolve(JSON.parse(response) as Record<string, unknown>),
|
||||
{ pairing }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe('pairing RPC methods', () => {
|
||||
it('passes only phone-owned credential material to the server-bound provider', async () => {
|
||||
const provisionRelay = vi.fn().mockResolvedValue({
|
||||
v: 1,
|
||||
reqId: 'install-1',
|
||||
authorizationMode: 'authenticated-direct',
|
||||
currentVersion: 1,
|
||||
resumeExpiresAt: Date.now() + 60_000
|
||||
})
|
||||
const pairing = { getEndpoints: vi.fn(), provisionRelay }
|
||||
|
||||
await expect(
|
||||
dispatchPairing(
|
||||
'pairing.provisionRelay',
|
||||
{ reqId: 'install-1', newResumeTokenHash: 'A'.repeat(43) },
|
||||
pairing
|
||||
)
|
||||
).resolves.toMatchObject({ ok: true })
|
||||
expect(provisionRelay).toHaveBeenCalledWith({
|
||||
reqId: 'install-1',
|
||||
newResumeTokenHash: 'A'.repeat(43)
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects caller-selected identity and authorization metadata', async () => {
|
||||
const pairing = { getEndpoints: vi.fn(), provisionRelay: vi.fn() }
|
||||
|
||||
for (const injected of [
|
||||
{ relayDeviceId: 'attacker-device' },
|
||||
{ authorization: { mode: 'relay-basis', basisConnId: 'attacker-basis' } },
|
||||
{ directAuthId: 'attacker-direct' },
|
||||
{ acceptedCredentialVersion: 99 }
|
||||
]) {
|
||||
await expect(
|
||||
dispatchPairing(
|
||||
'pairing.provisionRelay',
|
||||
{ reqId: 'install-1', newResumeTokenHash: 'A'.repeat(43), ...injected },
|
||||
pairing
|
||||
)
|
||||
).resolves.toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
||||
}
|
||||
await expect(
|
||||
dispatchPairing(
|
||||
'pairing.getEndpoints',
|
||||
{ installReqId: 'status-1', basisConnId: 'injected' },
|
||||
pairing
|
||||
)
|
||||
).resolves.toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
||||
expect(pairing.provisionRelay).not.toHaveBeenCalled()
|
||||
expect(pairing.getEndpoints).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineMethod, type RpcAnyMethod } from '../core'
|
||||
import {
|
||||
PairingGetEndpointsParamsSchema,
|
||||
PairingProvisionRelayParamsSchema
|
||||
} from '../../../../shared/mobile-relay-credential-contract'
|
||||
|
||||
export const PAIRING_METHODS: readonly RpcAnyMethod[] = [
|
||||
defineMethod({
|
||||
name: 'pairing.getEndpoints',
|
||||
params: PairingGetEndpointsParamsSchema,
|
||||
handler: async (params, ctx) => {
|
||||
if (!ctx.pairing) {
|
||||
throw new Error('pairing_context_unavailable')
|
||||
}
|
||||
return await ctx.pairing.getEndpoints(params)
|
||||
}
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'pairing.provisionRelay',
|
||||
params: PairingProvisionRelayParamsSchema,
|
||||
handler: async (params, ctx) => {
|
||||
if (!ctx.pairing) {
|
||||
throw new Error('pairing_context_unavailable')
|
||||
}
|
||||
return await ctx.pairing.provisionRelay(params)
|
||||
}
|
||||
})
|
||||
]
|
||||
Reference in New Issue
Block a user