Files
orca/src/shared/runtime-host-pairing-identity.ts
T
b80c420054 feat(pairing): re-pairing the same server keeps its environment (#27197)
* feat(pairing): re-pairing the same server keeps its environment

The web client minted a new environment id on every runtime-scope pairing link,
even for the same server key, so existing terminals kept the old id and failed
as ambiguous. A key-matched re-pair now keeps the id (bumping the pairing
revision) and pins the server's host descriptor; a same-key server with a
different installation gets a new id, and a downgraded server never replaces
the pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): a reply from the replaced pairing never overwrites a same-server re-pair

Re-pairing now keeps the environment id, so fence on the pairing revision too:
a request captured before the re-pair can neither reopen a client with the
replaced token nor save the old pairing back over the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 02:07:28 -07:00

48 lines
1.9 KiB
TypeScript

import type { RuntimeHostDescriptor } from './runtime-host-descriptor'
/**
* How a new pairing offer relates to a server this client already pairs with.
* `pin` is the descriptor to keep pinned. `descriptor`:
* - `pinned`: the offer's descriptor matches (or is now pinned for the first time).
* - `moved`: same installation, different machine binding: the profile moved or was reinstalled.
* - `unverifiable`: a pinned server now sends no descriptor (downgraded); the old pin is kept.
* - `unpinned`: neither side has a descriptor yet.
*/
export type RuntimeHostRePairMatch =
| {
kind: 'same-host'
pin: RuntimeHostDescriptor | undefined
descriptor: 'pinned' | 'moved' | 'unverifiable' | 'unpinned'
}
| { kind: 'different-host' }
/**
* The host key, which the pairing handshake proves, is the trust root: a different key is a
* different server. The descriptor only separates a reinstall or a deliberate clone that kept the key.
*/
export function classifyRuntimeHostRePair(
previous: { publicKeys: readonly string[]; pin?: RuntimeHostDescriptor },
offer: { publicKeyB64: string; hostDescriptor?: RuntimeHostDescriptor }
): RuntimeHostRePairMatch {
if (!previous.publicKeys.includes(offer.publicKeyB64)) {
return { kind: 'different-host' }
}
const { pin } = previous
const next = offer.hostDescriptor
if (!next) {
// Why keep the old pin: an older server must never let this client learn a replacement.
return { kind: 'same-host', pin, descriptor: pin ? 'unverifiable' : 'unpinned' }
}
if (!pin) {
return { kind: 'same-host', pin: next, descriptor: 'pinned' }
}
if (pin.installationId !== next.installationId) {
return { kind: 'different-host' }
}
const moved =
pin.machineBinding !== undefined &&
next.machineBinding !== undefined &&
pin.machineBinding !== next.machineBinding
return { kind: 'same-host', pin: next, descriptor: moved ? 'moved' : 'pinned' }
}