mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +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>
62 lines
2.0 KiB
TypeScript
62 lines
2.0 KiB
TypeScript
// Why: a plain-http page on another machine can be swapped by anyone on the LAN path, so a
|
|
// pairing code typed into it is exposed. Browsers treat loopback http as a secure context.
|
|
export function isInsecureRemoteWebOrigin(origin: string | undefined): boolean {
|
|
if (!origin) {
|
|
return false
|
|
}
|
|
let url: URL
|
|
try {
|
|
url = new URL(origin)
|
|
} catch {
|
|
return false
|
|
}
|
|
return url.protocol === 'http:' && !isLoopbackHostname(url.hostname)
|
|
}
|
|
|
|
function isLoopbackHostname(hostname: string): boolean {
|
|
const host = hostname.toLowerCase()
|
|
return (
|
|
host === 'localhost' ||
|
|
host.endsWith('.localhost') ||
|
|
host === '[::1]' ||
|
|
/^127(?:\.\d{1,3}){3}$/.test(host)
|
|
)
|
|
}
|
|
|
|
/** Sealed auth error for a first pairing from such a page without operator opt-in. */
|
|
export const INSECURE_WEB_ORIGIN_AUTH_CODE = 'insecure_origin' as const
|
|
|
|
// Operator opt-in, read by the host serving the page (desktop or `orca serve`).
|
|
export const ALLOW_INSECURE_WEB_PAIRING_ENV = 'ORCA_ALLOW_INSECURE_WEB_PAIRING' as const
|
|
|
|
// Why a marker: React Native phones also send an http Origin on LAN sockets, so the Origin alone
|
|
// cannot say "browser". Only the web bundle adds this query to its upgrade URL; hosts that predate
|
|
// it ignore the query string, so the marked upgrade still connects to them.
|
|
export const WEB_CLIENT_UPGRADE_PARAM = 'orca-client' as const
|
|
export const WEB_CLIENT_UPGRADE_VALUE = 'web' as const
|
|
|
|
export function withWebClientUpgradeMarker(endpoint: string): string {
|
|
try {
|
|
const url = new URL(endpoint)
|
|
url.searchParams.set(WEB_CLIENT_UPGRADE_PARAM, WEB_CLIENT_UPGRADE_VALUE)
|
|
return url.toString()
|
|
} catch {
|
|
return endpoint
|
|
}
|
|
}
|
|
|
|
/** True when an upgrade request path (`/path?query`) carries the web bundle's marker. */
|
|
export function hasWebClientUpgradeMarker(requestUrl: string | undefined): boolean {
|
|
if (!requestUrl) {
|
|
return false
|
|
}
|
|
try {
|
|
return (
|
|
new URL(requestUrl, 'http://upgrade.invalid').searchParams.get(WEB_CLIENT_UPGRADE_PARAM) ===
|
|
WEB_CLIENT_UPGRADE_VALUE
|
|
)
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|