mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* feat(runtime): session-only browser keys and an opt-in for pairing from plain-http pages (host) - pairing.registerDeviceKey accepts ephemeral: true (advertised as pairing.device-key-ephemeral.v1). Such keyed rows are dropped when the host restarts and after 12 hours with no live socket; a sweep removes them and cuts any socket still bound to them, so later key auth gets device_revoked. - The WebSocket upgrade Origin is recorded per socket. A first pairing (a never-used pairing code) from an http:// page on another machine is refused with a sealed insecure_origin error unless the operator sets ORCA_ALLOW_INSECURE_WEB_PAIRING=1. Already-paired browsers, https and loopback pages, and native clients are unchanged. - Registry file reading moved to device-registry-file-read.ts to keep the registry under the size cap. * fix(runtime): gate first pairings only for the marked web client, never phones React Native sends an http Origin on every LAN WebSocket, so the Origin alone refused first-time QR pairing from released phones. The web bundle now marks its upgrade with ?orca-client=web; only a marked socket from an http page on another machine is gated. Released hosts ignore the query string. --------- Co-authored-by: m4air <m4air@Mac.localdomain>
72 lines
3.4 KiB
TypeScript
72 lines
3.4 KiB
TypeScript
import { mkdtempSync, readFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import nacl from 'tweetnacl'
|
|
import { beforeAll, describe, expect, it } from 'vitest'
|
|
import { DeviceRegistry, type DeviceEntry } from '../../../src/main/runtime/device-registry'
|
|
import { DEVICE_REGISTRY_FILENAME } from '../../../src/main/runtime/mobile-pairing-files'
|
|
import { DEVICE_KEY_EPHEMERAL_RUNTIME_CAPABILITY } from '../../../src/shared/device-key-capability'
|
|
import {
|
|
importReleaseCheckoutModule,
|
|
materializeReleaseCheckout,
|
|
resolveBaselineReleaseRef
|
|
} from './release-checkout'
|
|
|
|
/**
|
|
* Session-only browser keys add an `ephemeral` flag to keyed rows. A released host (desktop
|
|
* downgrade) must keep such rows, keep token rows authenticating, and never advertise the
|
|
* capability that makes a browser send `ephemeral` to it.
|
|
*/
|
|
const SUITE_TIMEOUT_MS = 180_000
|
|
|
|
type RegistryLike = {
|
|
validateToken(token: string): { deviceId: string } | null
|
|
updateLastSeen(deviceId: string): void
|
|
}
|
|
|
|
let ReleasedRegistry: new (userDataPath: string) => RegistryLike
|
|
let releasedCapabilities: readonly string[]
|
|
|
|
beforeAll(async () => {
|
|
const checkout = await materializeReleaseCheckout(resolveBaselineReleaseRef())
|
|
const [registryModule, protocolModule] = await Promise.all([
|
|
importReleaseCheckoutModule(checkout, 'src/main/runtime/device-registry.ts'),
|
|
importReleaseCheckoutModule(checkout, 'src/shared/protocol-version.ts')
|
|
])
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release's DeviceRegistry class; a changed shape fails the assertions below.
|
|
ReleasedRegistry = registryModule.DeviceRegistry as typeof ReleasedRegistry
|
|
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the pinned release's capability list; a changed shape fails the assertion below.
|
|
releasedCapabilities = protocolModule.RUNTIME_CAPABILITIES as readonly string[]
|
|
}, SUITE_TIMEOUT_MS)
|
|
|
|
describe('cross-version: session-only browser rows', () => {
|
|
it('a released host never invites a browser to send ephemeral', () => {
|
|
expect(releasedCapabilities).not.toContain(DEVICE_KEY_EPHEMERAL_RUNTIME_CAPABILITY)
|
|
})
|
|
|
|
it('a released registry keeps the session row and token rows; this host drops it on restart', () => {
|
|
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-xv-web-session-'))
|
|
const current = new DeviceRegistry(userDataPath)
|
|
const legacy = current.addDevice('Browser', 'runtime')
|
|
current.updateLastSeen(legacy.deviceId)
|
|
const keyB64 = Buffer.from(nacl.sign.keyPair().publicKey).toString('base64')
|
|
const session = current.forkDeviceKey(legacy.deviceId, keyB64, 'Tab', { ephemeral: true })
|
|
|
|
const released = new ReleasedRegistry(userDataPath)
|
|
expect(released.validateToken(legacy.token)?.deviceId).toBe(legacy.deviceId)
|
|
released.updateLastSeen(legacy.deviceId)
|
|
const rows: DeviceEntry[] = JSON.parse(
|
|
readFileSync(join(userDataPath, DEVICE_REGISTRY_FILENAME), 'utf8')
|
|
)
|
|
expect(rows.find((row) => row.deviceId === session.deviceId)).toMatchObject({
|
|
ephemeral: true,
|
|
devicePublicKeyB64: keyB64
|
|
})
|
|
expect(rows.find((row) => row.deviceId === session.deviceId)?.token).toBeUndefined()
|
|
|
|
const restarted = new DeviceRegistry(userDataPath)
|
|
expect(restarted.getDevice(session.deviceId)).toBeNull()
|
|
expect(restarted.validateToken(legacy.token)?.deviceId).toBe(legacy.deviceId)
|
|
})
|
|
})
|