Files
orca/mobile/scripts/mock-server.ts
T
Jinjing 8e08e0f13a feat(native-chat): mark forks with a Fork prefix and a Forked from pill (#42370)
* feat(native-chat): mark forks with a Fork prefix and a Forked from pill

Derive the fork mark from the chat's forked-from link instead of the saved
name: new forks keep their source's name, tabs and chat lists show a muted
"Fork ·" prefix (a fork icon in narrow tabs), and the transcript divider
becomes a pill that opens the original. Older " fork" names are trimmed
only when they exactly match what the old build generated.

* fix(native-chat): open fork sources in local workspaces; fork mark polish

- Open the source from the Forked from pill whenever its host is reachable:
  a local workspace or a connected SSH target, and a connected remote runtime.
- Draw the Fork mark in the muted text color (not opacity), normal weight,
  and name a forked source with its mark in the tab hover card.
- Name a fork tab's close button after its marked title.
- Shorter ru/pt mark words; bdi around names; RTL-aware pill chevron.
- Re-read a watched source's parent when its marker refreshes.
- Mock server: MOCK_FORKED_CHATS scenario for the phone.

* fix(native-chat): name forked phone rows for screen readers; read source agent only

- Mobile worktree rows give a forked chat's label the "Fork: <name>" accessible name.
- An older fork's unnamed source reads the agent from the source's own summary,
  which its subscription already wakes for.

* fix(mobile): carry the fork mark on history rows, not a separate set

Session-history recordings capture the screen state as plain data; a fork
is now an optional field on its own row, so rows without forks are unchanged.

* test: drop copy-pinning fork tooltip cases

* fix(native-chat): show the fork icon on narrow tabs in the app

A later stylesheet repeats Tailwind's hidden utility, which beat the icon's
container-query display rule, so narrow fork tabs showed no mark. The icon
now renders only in tabs and uses its own display rules.

* chore(mobile): mock server answers a v2 hello like an older host

Current phones open with the v2 handshake; closing with 4001 lets them fall
back to the legacy handshake this mock implements.

* fix(mobile): mark a forked chat in its tab's action sheet title

* test(mobile): assert the fork sheet title's behavior, not its copy

* test(mobile): type the fork sheet test's stand-in props

* test: follow main's moved test rig and async fork-marker delivery
2026-11-14 12:36:12 -08:00

136 lines
4.5 KiB
TypeScript

#!/usr/bin/env npx tsx
// Standalone E2EE WebSocket mock exposes desktop RPCs with fake data for mobile development without Orca.
import { WebSocketServer, type WebSocket } from 'ws'
import { deriveSharedKey, e2eeDecrypt, e2eeEncrypt, type E2EEState } from './mock-server-encryption'
import { loadOrCreateMockServerKeyPair } from './mock-server-key-pair'
import {
error,
handleRequest,
mockScenarioSummary,
type RpcRequest
} from './mock-server-rpc-handlers'
const PORT = Number(process.env.PORT) || 6768
const AUTH_TOKEN = 'mock-device-token'
// Print server public key for pairing QR; MOCK_SERVER_KEY_FILE preserves it across restarts for pinned devices.
const serverKeyPair = loadOrCreateMockServerKeyPair(process.env.MOCK_SERVER_KEY_FILE)
const serverPublicKeyB64 = Buffer.from(serverKeyPair.publicKey).toString('base64')
const wss = new WebSocketServer({ port: PORT })
// First frame is plaintext e2ee_hello; encrypt subsequent RPCs with derived shared key.
const connectionState = new Map<WebSocket, E2EEState>()
wss.on('connection', (ws) => {
console.log('[mock] Client connected — waiting for e2ee_hello')
ws.on('message', (data) => {
const msg = typeof data === 'string' ? data : data.toString('utf-8')
const e2ee = connectionState.get(ws)
if (!e2ee) {
let hello: { type?: string; v?: number; publicKeyB64?: string }
try {
hello = JSON.parse(msg)
} catch {
ws.send(JSON.stringify({ type: 'e2ee_error', message: 'Invalid JSON' }))
ws.close()
return
}
// Answer a v2 hello as an older host does (4001), so the phone retries with the legacy handshake.
if (hello.type === 'e2ee_hello' && hello.v === 2) {
ws.close(4001)
return
}
if (hello.type !== 'e2ee_hello' || !hello.publicKeyB64) {
ws.send(JSON.stringify({ type: 'e2ee_error', message: 'Expected e2ee_hello' }))
ws.close()
return
}
const clientPublicKey = Uint8Array.from(Buffer.from(hello.publicKeyB64, 'base64'))
if (clientPublicKey.length !== 32) {
ws.send(JSON.stringify({ type: 'e2ee_error', message: 'Invalid public key' }))
ws.close()
return
}
const sharedKey = deriveSharedKey(serverKeyPair.secretKey, clientPublicKey)
connectionState.set(ws, { sharedKey, deviceToken: null, authenticated: false })
ws.send(JSON.stringify({ type: 'e2ee_ready' }))
console.log('[mock] E2EE key exchange complete — waiting for encrypted auth')
return
}
const plaintext = e2eeDecrypt(msg, e2ee.sharedKey)
if (plaintext === null) {
console.log('[mock] Decryption failed — dropping message')
return
}
let request: RpcRequest
try {
request = JSON.parse(plaintext) as RpcRequest
} catch {
const encrypted = e2eeEncrypt(
JSON.stringify(error('unknown', 'bad_request', 'Invalid JSON')),
e2ee.sharedKey
)
ws.send(encrypted)
return
}
if (!e2ee.authenticated) {
const auth = request as unknown as { type?: string; deviceToken?: string }
if (auth.type !== 'e2ee_auth' || auth.deviceToken !== AUTH_TOKEN) {
ws.send(
e2eeEncrypt(
JSON.stringify({ type: 'e2ee_error', error: { code: 'unauthorized' } }),
e2ee.sharedKey
)
)
ws.close()
return
}
e2ee.deviceToken = auth.deviceToken
e2ee.authenticated = true
ws.send(e2eeEncrypt(JSON.stringify({ type: 'e2ee_authenticated' }), e2ee.sharedKey))
console.log('[mock] E2EE authentication complete')
return
}
console.log(`[mock] ${request.method} (id: ${request.id})`)
handleRequest(
request,
(response) => {
if (ws.readyState === ws.OPEN) {
ws.send(e2eeEncrypt(JSON.stringify(response), e2ee.sharedKey))
}
},
ws
)
})
ws.on('close', () => {
connectionState.delete(ws)
console.log('[mock] Client disconnected')
})
ws.on('error', () => {
connectionState.delete(ws)
ws.close()
})
})
console.log(`[mock] Orca mock server listening on ws://localhost:${PORT}`)
console.log(`[mock] Auth token: ${AUTH_TOKEN}`)
console.log(`[mock] Server public key (base64): ${serverPublicKeyB64}`)
console.log(
`[mock] Scenario: ${mockScenarioSummary.repoCount} repos, ${mockScenarioSummary.worktreeCount} worktrees, ${mockScenarioSummary.rpcDelayMs}ms default RPC delay`
)
console.log(`[mock] E2EE enabled — clients must send e2ee_hello before RPC`)