mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 17:37:40 +00:00
329 lines
16 KiB
TypeScript
329 lines
16 KiB
TypeScript
// MVP checks for the edge on its real backend: the cells' credential store over (local) Hyperdrive,
|
|
// a fetched JWKS with issuer check, and the director's assignment row. Drives the SHIPPED desktop
|
|
// control client (commands and their strict response parsers) and the shipped phone hello parser.
|
|
//
|
|
// Needs a local Postgres with the relay schema (see test/apply-schema.ts) at the URL in
|
|
// wrangler.jsonc's localConnectionString, and its data directory for the database-down check:
|
|
// MVP_PGDATA=/tmp/relay-edge-mvp-pg npx tsx test/mvp.ts
|
|
import { spawn, execSync, type ChildProcess } from 'node:child_process'
|
|
import { createHash, randomBytes } from 'node:crypto'
|
|
import { createServer } from 'node:http'
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { SignJWT, exportJWK, generateKeyPair, type KeyLike } from 'jose'
|
|
import nacl from 'tweetnacl'
|
|
import pg from 'pg'
|
|
import WebSocket from 'ws'
|
|
import { RelayControlClient } from '../../../../src/main/runtime/mobile-cloud-relay/control-channel/relay-control-client'
|
|
import { CloudRelayTransport } from '../../../../src/main/runtime/rpc/transports/relay-transport'
|
|
import { RelayPhoneHelloSchema } from '../../../../src/shared/remote-runtime/relay/mobile-relay-phone-protocol'
|
|
|
|
const PORT = 8788
|
|
const ORIGIN = `http://127.0.0.1:${PORT}`
|
|
const DB_URL = process.env.MVP_DB_URL ?? 'postgres://relay:local-test@127.0.0.1:55433/relay'
|
|
const PGDATA = process.env.MVP_PGDATA ?? '/tmp/relay-edge-mvp-pg'
|
|
const PGPORT = new URL(DB_URL).port
|
|
const ISSUER = 'https://auth.test.invalid'
|
|
const EPOCH = 7
|
|
const USER = 'user-mvp'
|
|
|
|
const results: { name: string; ok: boolean; detail: string }[] = []
|
|
function record(name: string, ok: boolean, detail = ''): void {
|
|
results.push({ name, ok, detail })
|
|
console.log(`${ok ? 'PASS' : 'FAIL'} ${name} ${detail}`)
|
|
}
|
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
const b64url = (b: Uint8Array) => Buffer.from(b).toString('base64url')
|
|
const sha = (s: string) => createHash('sha256').update(s).digest('base64url')
|
|
async function waitFor(cond: () => boolean, ms: number): Promise<boolean> {
|
|
const end = Date.now() + ms
|
|
while (Date.now() < end) {
|
|
if (cond()) { return true }
|
|
await sleep(25)
|
|
}
|
|
return cond()
|
|
}
|
|
|
|
type Host = { relayHostId: string; keypair: { publicKey: Uint8Array; secretKey: Uint8Array; publicKeyB64: string } }
|
|
|
|
function newHost(): Host {
|
|
const box = nacl.box.keyPair()
|
|
return {
|
|
relayHostId: createHash('sha256').update(box.publicKey).digest('base64url').slice(0, 16),
|
|
keypair: { publicKey: box.publicKey, secretKey: box.secretKey, publicKeyB64: Buffer.from(box.publicKey).toString('base64') }
|
|
}
|
|
}
|
|
|
|
async function mint(key: KeyLike, relayHostId: string, issuer = ISSUER): Promise<string> {
|
|
return new SignJWT({ purpose: 'host-control', prof: `prof-${USER}`, relayHostId })
|
|
.setProtectedHeader({ alg: 'ES256', kid: 'mvp' })
|
|
.setSubject(USER)
|
|
.setIssuer(issuer)
|
|
.setAudience('orca-relay')
|
|
.setIssuedAt()
|
|
.setExpirationTime('5m')
|
|
.sign(key)
|
|
}
|
|
|
|
function control(host: Host, jwt: string, epoch = EPOCH) {
|
|
const events = { closes: [] as number[], opens: [] as { connId: string }[] }
|
|
let transport: CloudRelayTransport | null = null
|
|
const client = new RelayControlClient({
|
|
cellUrl: ORIGIN,
|
|
relayJwt: jwt,
|
|
relayHostId: host.relayHostId,
|
|
assignmentEpoch: epoch,
|
|
identity: { userId: USER, profileId: `prof-${USER}`, organizationId: '' },
|
|
keypair: host.keypair as never,
|
|
appVersion: 'mvp',
|
|
onConnectionOpen: (m) => {
|
|
events.opens.push(m)
|
|
void transport?.openConnection(m)
|
|
},
|
|
onDrain: () => {},
|
|
onClose: (code) => events.closes.push(code),
|
|
createSocket: (url, token) =>
|
|
new WebSocket(url, {
|
|
headers: { authorization: `Bearer ${token}`, 'x-orca-host-capabilities': 'pending-conn-details,idle-regional-rehome-v1' },
|
|
perMessageDeflate: false,
|
|
maxPayload: 64 * 1024
|
|
})
|
|
})
|
|
return {
|
|
client,
|
|
events,
|
|
attach(generation: number) {
|
|
transport = new CloudRelayTransport({ cellUrl: ORIGIN, relayHostId: host.relayHostId, generation })
|
|
transport.onMessage(((m: string | Uint8Array, reply: (r: string | Uint8Array) => void) => reply(m)) as never)
|
|
return transport
|
|
}
|
|
}
|
|
}
|
|
|
|
function phone(relayHostId: string, credential: string) {
|
|
const ws = new WebSocket(`ws://127.0.0.1:${PORT}/v1/connect/${relayHostId}`, { perMessageDeflate: false })
|
|
const state = { hello: null as Record<string, unknown> | null, closed: null as number | null, reason: '' }
|
|
ws.on('open', () => ws.send(JSON.stringify({ type: 'relay-auth', v: 1, mode: 'connect', credential })))
|
|
ws.on('message', (raw) => {
|
|
if (state.hello === null) { state.hello = JSON.parse(raw.toString()) }
|
|
})
|
|
ws.on('close', (code, reason) => {
|
|
state.closed = code
|
|
state.reason = reason.toString()
|
|
})
|
|
ws.on('error', () => {})
|
|
return { ws, state }
|
|
}
|
|
|
|
async function phoneOutcome(relayHostId: string, credential: string) {
|
|
const p = phone(relayHostId, credential)
|
|
await waitFor(() => p.state.hello !== null || p.state.closed !== null, 8000)
|
|
return p
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const db = new pg.Client({ connectionString: DB_URL })
|
|
// The database-down check stops Postgres under this client.
|
|
db.on('error', () => {})
|
|
await db.connect()
|
|
for (const table of [
|
|
'relay_assignments', 'relay_devices', 'relay_invites', 'relay_connection_bases', 'relay_confirm_results',
|
|
'relay_install_results', 'relay_audit_events', 'relay_rate_windows', 'relay_direct_authorizations'
|
|
]) {
|
|
await db.query(`DELETE FROM ${table}`)
|
|
}
|
|
const host = newHost()
|
|
const now = Date.now()
|
|
const seat = (cellId: string) =>
|
|
db.query(
|
|
`INSERT INTO relay_assignments (user_id, relay_host_id, cell_id, assignment_epoch, lease_expires_at, last_activity_at,
|
|
reserved_controls, reserved_splices, reserved_invites, pending_installs, pending_confirmations, migration_leases)
|
|
VALUES ($1, $2, $3, $4, $5, $6, 0, 0, 0, 0, 0, 0)
|
|
ON CONFLICT (user_id, relay_host_id) DO UPDATE SET cell_id = EXCLUDED.cell_id`,
|
|
[USER, host.relayHostId, cellId, EPOCH, now + 3_600_000, now]
|
|
)
|
|
await seat('edge')
|
|
|
|
// ---- JWKS served over HTTP, as the auth service does ----
|
|
const { publicKey, privateKey } = await generateKeyPair('ES256', { extractable: true })
|
|
const jwk = { ...(await exportJWK(publicKey)), kid: 'mvp', alg: 'ES256', use: 'sig' }
|
|
let jwksHits = 0
|
|
const jwks = createServer((_req, res) => {
|
|
jwksHits++
|
|
res.setHeader('content-type', 'application/json')
|
|
res.end(JSON.stringify({ keys: [jwk] }))
|
|
})
|
|
await new Promise<void>((r) => jwks.listen(0, '127.0.0.1', () => r()))
|
|
const jwksUrl = `http://127.0.0.1:${(jwks.address() as { port: number }).port}/jwks`
|
|
|
|
writeFileSync(
|
|
new URL('../.dev.vars', import.meta.url),
|
|
[
|
|
'EDGE_BACKEND=postgres',
|
|
`RELAY_JWKS_URL=${jwksUrl}`,
|
|
`RELAY_AUTH_ISSUER=${ISSUER}`,
|
|
'PING_INTERVAL_MS=3000',
|
|
'SILENCE_TIMEOUT_MS=9000',
|
|
'ORPHAN_GRACE_MS=4000'
|
|
].join('\n')
|
|
)
|
|
const stateDir = mkdtempSync(join(tmpdir(), 'relay-edge-mvp-'))
|
|
const dev: ChildProcess = spawn(
|
|
'npx',
|
|
['wrangler', 'dev', '--port', String(PORT), '--ip', '127.0.0.1', '--persist-to', stateDir],
|
|
{ cwd: new URL('..', import.meta.url).pathname, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
)
|
|
let devLog = ''
|
|
dev.stdout?.on('data', (d) => (devLog += d))
|
|
dev.stderr?.on('data', (d) => (devLog += d))
|
|
try {
|
|
for (let i = 0; ; i++) {
|
|
try {
|
|
if ((await fetch(`${ORIGIN}/health`)).ok) { break }
|
|
} catch {}
|
|
if (i > 120) { throw new Error(`wrangler dev did not start:\n${devLog}`) }
|
|
await sleep(500)
|
|
}
|
|
await run(host, privateKey, seat, () => jwksHits)
|
|
} finally {
|
|
// Never leave the database stopped.
|
|
try {
|
|
execSync(`pg_ctl -D ${PGDATA} status`, { stdio: 'ignore' })
|
|
} catch {
|
|
execSync(`pg_ctl -D ${PGDATA} -o "-p ${PGPORT} -k /tmp" -l ${PGDATA}.log start`, { stdio: 'ignore' })
|
|
}
|
|
dev.kill('SIGINT')
|
|
jwks.close()
|
|
await db.end().catch(() => {})
|
|
await sleep(500)
|
|
rmSync(stateDir, { recursive: true, force: true })
|
|
}
|
|
const failed = results.filter((r) => !r.ok)
|
|
console.log(`\n${results.length - failed.length}/${results.length} passed`)
|
|
if (failed.length) { process.exitCode = 1 }
|
|
}
|
|
|
|
async function run(
|
|
host: Host,
|
|
key: KeyLike,
|
|
seat: (cellId: string) => Promise<unknown>,
|
|
jwksHits: () => number
|
|
): Promise<void> {
|
|
const jwt = await mint(key, host.relayHostId)
|
|
|
|
// E1: fetched JWKS + issuer; wrong issuer is 401 before the upgrade.
|
|
{
|
|
const bad = new WebSocket(`ws://127.0.0.1:${PORT}/v1/host/control`, {
|
|
headers: { authorization: `Bearer ${await mint(key, host.relayHostId, 'https://evil.invalid')}` }
|
|
})
|
|
const status = await new Promise<number>((res) => bad.on('unexpected-response', (_q, r) => res(r.statusCode ?? 0)))
|
|
record('E1 JWKS fetched from URL; wrong issuer refused 401', status === 401 && jwksHits() >= 1, `status=${status} jwksFetches=${jwksHits()}`)
|
|
}
|
|
|
|
// E1: assignment epoch check against the director's row.
|
|
{
|
|
const wrong = control(host, jwt, EPOCH - 1)
|
|
const r = await wrong.client.connect().catch((e: Error) => e)
|
|
await waitFor(() => wrong.events.closes.length > 0, 3000)
|
|
record('E1 hello at a stale epoch closes 4409 (wrong assignment epoch)', r instanceof Error && wrong.events.closes.includes(4409), `closes=${wrong.events.closes}`)
|
|
}
|
|
|
|
const main = control(host, jwt)
|
|
const ack = await main.client.connect()
|
|
main.attach(ack.generation)
|
|
record('E1 hello at the assigned epoch is accepted (shipped client)', ack.generation >= 1, `generation=${ack.generation}`)
|
|
|
|
// E2: invite-create through the shipped client's strict parser.
|
|
const invite = await main.client.createInvite('device-a').catch((e: Error) => e)
|
|
record('E2 invite-create returns a strict invite-created', !(invite instanceof Error) && typeof invite.inviteToken === 'string', invite instanceof Error ? invite.message : `expiresIn=${Math.round((invite.expiresAt - Date.now()) / 1000)}s max=${invite.maxAttempts}`)
|
|
if (invite instanceof Error) { return }
|
|
|
|
// E2: phone connects with the invite; shipped phone parser accepts relay-hello(invite).
|
|
const invitePhone = await phoneOutcome(host.relayHostId, invite.inviteToken)
|
|
const inviteHello = RelayPhoneHelloSchema.safeParse(invitePhone.state.hello)
|
|
const inviteConnId = main.events.opens.at(-1)?.connId ?? ''
|
|
record('E2 invite phone admitted, relay-hello(invite) strict-parses', inviteHello.success && invitePhone.state.hello?.ok === true && invitePhone.state.hello?.credentialKind === 'invite', JSON.stringify(invitePhone.state.hello))
|
|
|
|
// E2: install a resume credential on the invite's basis; repeat is idempotent; status committed.
|
|
const resumeToken = b64url(randomBytes(32))
|
|
const install = { reqId: 'install-1', relayDeviceId: 'device-a', newResumeTokenHash: sha(resumeToken), authorization: { mode: 'relay-basis' as const, basisConnId: inviteConnId } }
|
|
const first = await main.client.installCredential(install).catch((e: Error) => e)
|
|
const again = await main.client.installCredential(install).catch((e: Error) => e)
|
|
const status = await main.client.credentialInstallStatus('device-a', 'install-1').catch((e: Error) => e)
|
|
record(
|
|
'E2 device-credential-install on relay basis; same reqId idempotent; status committed',
|
|
!(first instanceof Error) && !(again instanceof Error) && JSON.stringify(first) === JSON.stringify(again) && !(status instanceof Error) && status.state === 'committed',
|
|
first instanceof Error ? first.message : again instanceof Error ? again.message : `version=${first.currentVersion} status=${status instanceof Error ? status.message : status.state}`
|
|
)
|
|
invitePhone.ws.close()
|
|
|
|
// E2: resume phone; confirm; repeat confirm idempotent.
|
|
const resumePhone = await phoneOutcome(host.relayHostId, resumeToken)
|
|
const resumeHello = RelayPhoneHelloSchema.safeParse(resumePhone.state.hello)
|
|
const resumeConnId = main.events.opens.at(-1)?.connId ?? ''
|
|
record('E2 resume phone admitted, relay-hello(resume) strict-parses', resumeHello.success && resumePhone.state.hello?.credentialKind === 'resume', JSON.stringify(resumePhone.state.hello))
|
|
const c1 = await main.client.confirmResume(resumeConnId, 'confirm-1').catch((e: Error) => e)
|
|
const c2 = await main.client.confirmResume(resumeConnId, 'confirm-1').catch((e: Error) => e)
|
|
record('E2 device-resume-confirm commits; same reqId returns the stored result', !(c1 instanceof Error) && !(c2 instanceof Error) && JSON.stringify(c1) === JSON.stringify(c2), c1 instanceof Error ? c1.message : c2 instanceof Error ? c2.message : `acceptedAs=${c1.acceptedAs} renewed=${c1.renewed}`)
|
|
resumePhone.ws.close()
|
|
|
|
// E2: a token the database doesn't know is 4401 (strict relay-hello ok:false).
|
|
const unknown = await phoneOutcome(host.relayHostId, b64url(randomBytes(32)))
|
|
await waitFor(() => unknown.state.closed !== null, 2000)
|
|
record('E2 unknown credential: relay-hello{ok:false,4401} and close 4401', unknown.state.hello?.ok === false && unknown.state.hello?.code === 4401 && unknown.state.closed === 4401 && RelayPhoneHelloSchema.safeParse(unknown.state.hello).success, `hello=${JSON.stringify(unknown.state.hello)} close=${unknown.state.closed}`)
|
|
|
|
// E2: host offline. Assignment names the edge -> 4404; names another cell -> 4409 (phone re-resolves).
|
|
main.client.closeNow()
|
|
await sleep(500)
|
|
const offline = await phoneOutcome(host.relayHostId, resumeToken)
|
|
await waitFor(() => offline.state.closed !== null, 2000)
|
|
await seat('c10')
|
|
const moved = await phoneOutcome(host.relayHostId, resumeToken)
|
|
await waitFor(() => moved.state.closed !== null, 2000)
|
|
await seat('edge')
|
|
record('E2 missing seat: 4404 when assigned here, 4409 when assigned elsewhere', offline.state.closed === 4404 && moved.state.closed === 4409, `here=${offline.state.closed}(${offline.state.reason}) elsewhere=${moved.state.closed}`)
|
|
|
|
// E2: revoke; the revoked token is now refused (4401).
|
|
const back = control(host, jwt)
|
|
const ack2 = await back.client.connect()
|
|
back.attach(ack2.generation)
|
|
const revoked = await back.client.revokeDevice('device-a').then(() => 'ok').catch((e: Error) => e.message)
|
|
const afterRevoke = await phoneOutcome(host.relayHostId, resumeToken)
|
|
await waitFor(() => afterRevoke.state.closed !== null, 2000)
|
|
record('E2 device-revoke, then the revoked resume token is refused 4401', revoked === 'ok' && afterRevoke.state.closed === 4401, `revoke=${revoked} close=${afterRevoke.state.closed}`)
|
|
|
|
// E2: database down. Phones get 4503 (never 4401), commands get control-error database_transient,
|
|
// a new hello is closed 4429. Then the database comes back and phones are admitted again.
|
|
const invite2 = await back.client.createInvite('device-b').catch((e: Error) => e)
|
|
execSync(`pg_ctl -D ${PGDATA} -m fast stop`, { stdio: 'ignore' })
|
|
try {
|
|
const downPhone = await phoneOutcome(host.relayHostId, b64url(randomBytes(32)))
|
|
await waitFor(() => downPhone.state.closed !== null, 8000)
|
|
const downCmd = await back.client.createInvite('device-c').then(() => 'ok').catch((e: Error) => e.message)
|
|
const fresh = control(host, jwt)
|
|
await fresh.client.connect().catch(() => null)
|
|
await waitFor(() => fresh.events.closes.length > 0, 8000)
|
|
record(
|
|
'E2 database down: phone 4503 (not 4401), command database_transient, new hello 4429',
|
|
downPhone.state.closed === 4503 && /database_transient/.test(downCmd) && fresh.events.closes.includes(4429),
|
|
`phone=${downPhone.state.closed} cmd=${downCmd} hello=${fresh.events.closes}`
|
|
)
|
|
} finally {
|
|
execSync(`pg_ctl -D ${PGDATA} -o "-p ${PGPORT} -k /tmp" -l ${PGDATA}.log start`, { stdio: 'ignore' })
|
|
await sleep(1500)
|
|
}
|
|
if (!(invite2 instanceof Error)) {
|
|
const recovered = await phoneOutcome(host.relayHostId, invite2.inviteToken)
|
|
record('E2 database back: invite phone admitted again', recovered.state.hello?.ok === true, JSON.stringify(recovered.state.hello))
|
|
recovered.ws.close()
|
|
}
|
|
|
|
back.client.closeNow()
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|