feat(settings): add Codex link controls and authenticated phone bridge
This commit is contained in:
1 parent
1acb5dd90f
commit
55a1beaf9d
22 files changed
+2857
-3
No files matched your search
@@ -42,6 +42,9 @@ import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
import net from 'node:net'
|
||||
import type { Duplex } from 'node:stream'
|
||||
import { CODEX_LINK_REQUEST_BYTES, validateControlRequest } from '../codex-link-control'
|
||||
import { parseStrictJson } from '../codex-link-json'
|
||||
import type { CodexLinkControlRequest, CodexLinkControlResponse } from '../codex-link-control.types'
|
||||
|
||||
import { loadOrCreateLanIdentity, type MobileLanIdentity, type MobileLanVault } from './mobile-lan-cert'
|
||||
import {
|
||||
@@ -706,6 +709,7 @@ export interface MobileDeviceView {
|
||||
export type PairingEndReason = 'claimed' | 'locked' | 'expired' | 'cancelled'
|
||||
|
||||
export interface LanProxyOptions {
|
||||
codexLinkControl?: (request: CodexLinkControlRequest) => Promise<CodexLinkControlResponse>
|
||||
tls: { key: string; cert: string }
|
||||
/** Must resolve the LOCAL backend or throw (remote mode, backend down). */
|
||||
resolveBackend: (profile: string) => Promise<MobileBackend>
|
||||
@@ -1108,6 +1112,35 @@ export function createLanProxy(options: LanProxyOptions) {
|
||||
return reply(req, res, path, 403, { error: 'forbidden' })
|
||||
}
|
||||
|
||||
if (path === '/api/aiturk/codex-link/control') {
|
||||
if (req.method !== 'POST') return reply(req, res, path, 405, { error: 'method_not_allowed' })
|
||||
if (!options.codexLinkControl) return reply(req, res, path, 503, { error: 'codex_link_unavailable' })
|
||||
if (!/^application\/json(?:;|$)/i.test(String(req.headers['content-type'] ?? ''))) {
|
||||
return reply(req, res, path, 400, { error: 'invalid_request' })
|
||||
}
|
||||
let request: CodexLinkControlRequest
|
||||
try {
|
||||
if (Number(req.headers['content-length'] ?? 0) > CODEX_LINK_REQUEST_BYTES) throw new Error('large_body')
|
||||
let size = 0
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of req) {
|
||||
size += chunk.length
|
||||
if (size > CODEX_LINK_REQUEST_BYTES) throw new Error('large_body')
|
||||
chunks.push(Buffer.from(chunk))
|
||||
}
|
||||
const parsed = parseStrictJson(Buffer.concat(chunks), CODEX_LINK_REQUEST_BYTES)
|
||||
validateControlRequest(parsed)
|
||||
request = parsed
|
||||
} catch { return reply(req, res, path, 400, { error: 'invalid_request' }) }
|
||||
// Pairing can be revoked while a slow body is being read.
|
||||
if (!devices.has(device.tokenHash)) return reply(req, res, path, 401, { error: 'unauthorized' })
|
||||
try {
|
||||
const result = await options.codexLinkControl(request)
|
||||
if (!devices.has(device.tokenHash)) return reply(req, res, path, 401, { error: 'unauthorized' })
|
||||
return reply(req, res, path, 200, result)
|
||||
} catch { return reply(req, res, path, 503, { error: 'codex_link_outcome_unknown' }) }
|
||||
}
|
||||
|
||||
const backend = await localBackend(url)
|
||||
|
||||
if (res.destroyed) return
|
||||
@@ -1510,6 +1543,7 @@ export type MobileLinkEvent =
|
||||
{ type: 'state'; state: MobileLinkState } | { type: 'paired'; name: string; state: MobileLinkState }
|
||||
|
||||
export interface MobileLinkDeps {
|
||||
codexLinkControl?: (request: CodexLinkControlRequest) => Promise<CodexLinkControlResponse>
|
||||
vault: MobileLanVault
|
||||
store: MobileLinkStore
|
||||
resolveBackend: (profile: string) => Promise<MobileBackend>
|
||||
@@ -1820,6 +1854,7 @@ export function createMobileLink(deps: MobileLinkDeps) {
|
||||
}
|
||||
|
||||
proxy = createLanProxy({
|
||||
codexLinkControl: deps.codexLinkControl,
|
||||
tls: identity,
|
||||
resolveBackend: async profile => {
|
||||
if (deps.isRemoteMode()) {
|
||||
|
||||
Reference in new issue
Block a user