Repository navigation
feat(pairing): single-use, hashed pairing tickets with owner approval hook and orca pairing commands - #27558
Draft
OrcaWin wants to merge 5 commits into
Draft
feat(pairing): single-use, hashed pairing tickets with owner approval hook and orca pairing commands#27558OrcaWin wants to merge 5 commits into
orca pairing commands#27558OrcaWin wants to merge 5 commits into
Conversation
OrcaWin
force-pushed
the
np/d2b-connection-handle
branch
from
October 11, 2026 19:42
69eb366 to
bb54fd8
Compare
OrcaWin
force-pushed
the
np/d2b-tickets
branch
from
October 11, 2026 19:56
166c318 to
ec1816d
Compare
7 tasks done
OrcaWin
force-pushed
the
np/d2b-connection-handle
branch
from
October 11, 2026 20:20
bb54fd8 to
1e289ba
Compare
OrcaWin
force-pushed
the
np/d2b-tickets
branch
from
October 11, 2026 20:26
ec1816d to
b8376ed
Compare
OrcaWin
changed the base branch from
np/d2b-connection-handle
to
np/d2b-web-keys
October 11, 2026 20:27
7 tasks done
OrcaWin
force-pushed
the
np/d2b-tickets
branch
from
October 11, 2026 21:03
11c96f9 to
53d22c6
Compare
OrcaWin
force-pushed
the
np/d2b-web-keys
branch
from
October 11, 2026 21:03
e6a3ace to
82d0701
Compare
OrcaWin
force-pushed
the
np/d2b-tickets
branch
from
October 11, 2026 21:58
20a3b26 to
24edc09
Compare
OrcaWin
force-pushed
the
np/d2b-web-keys
branch
from
October 11, 2026 21:58
82d0701 to
4be29a4
Compare
added 5 commits
October 11, 2026 15:17
…reate/list/revoke` The host now mints opaque single-use tickets kept in memory as sha256 only. A client redeems one inside an E2EE v2 session with a fresh Ed25519 key and a proof over that session's transcript; the ticket is locked synchronously, the keyed device row is written durably before the reply, and a second redemption is refused as ticket_consumed. Tickets the desktop UI mints wait for the owner's approval, which shows a comparison code both screens derive from the transcript and requires typing it before a same-named device is replaced. Headless tickets come from the new owner-only `orca pairing` commands: TTL defaults to 30m and anything above 1h is rejected. The secret only appears in the printed link's fragment. Hosts advertise pairing.ticket.v1; the QR dialog still hands out the legacy offer, so existing phones are unaffected.
… never revokes its pending key - `orca pairing create` prints the verified https link (secret in the fragment); any app can claim orca://, and a headless ticket needs no approval. Ticket input accepts only a bare code or that link. - An unexpected throw after the lock burns the ticket instead of leaving it mid-redemption, and key auth for a still-reserved id answers registry_unavailable, never device_revoked. - Replacement matching sanitizes the stored device name too.
OrcaWin
force-pushed
the
np/d2b-tickets
branch
from
October 11, 2026 22:18
24edc09 to
2643563
Compare
3 tasks done
6 tasks done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
Today a pairing code is a reusable password: whoever holds the QR or link can connect as that device for good. This PR adds pairing tickets instead. A ticket works once and expires soon. Redeeming one creates a new device that signs in with its own private key, not a shared code.
It also adds owner-only commands to manage tickets from a terminal:
orca pairing createmakes a ticket;orca pairing listshows the outstanding ones;orca pairing revokecancels one.Phones and the QR dialog are unchanged in this PR: the QR still hands out today's code, so every existing phone keeps pairing and connecting as before.
What Changed
Before:
orca-devices.json.After (host side):
src/main/runtime/pairing-ticket-book.ts, in memory only).sha256(ticket).{ticket, deviceId, devicePublicKeyB64, deviceProofB64, deviceLabel}.redeemover this connection's v2 transcript, so a captured proof is useless on another connection.ticket_consumed.device_key_in_use.ephemeral: true(sent by session-only browsers) makes the new row session-only, so the host drops it on restart and after idle time.bad_auth.base32(HKDF-SHA256(transcriptHash, "orca-pair-sas/v1"))[0..6]. The client derives the same code from the same transcript, so a man in the middle shows a different code.registry_unavailable, neverdevice_revoked, so a retrying client keeps its pending key. Once the ticket is spent, an absent row is a real revocation. This PR adds the approval hook (setPairingTicketApprover). The desktop dialog that fills it comes next; until then, UI-minted tickets are denied and nothing mints them yet.orca pairing create [--ttl 30m] [--json],orca pairing listandorca pairing revoke <id>.pairing-admin(never granted to paired runtimes, and not on the phone allowlist), and they also need an owner-only context that only the local owner transport sets.createprints only the verified https link (https://www.onorca.dev/pair#<code>) on stdout, with the secret in the fragment. A status line without the secret goes to stderr.orca://…: any app can claim that scheme, and a headless ticket redeems without approval. Ticket input accepts only a bare code or the verified link.--environmentand--pairing-codeflags are refused.ticket_invalid,ticket_consumed,approval_denied,device_key_in_use.pairing.ticket.v1. The new offer format is a v3 code (src/shared/pairing-ticket-offer.ts), which today's v2 parser rejects, so an older app reports an invalid code rather than misreading it.e2ee-channel.tslost its hello parsing and legacy inbound handling to two helpers. Behaviour is unchanged and covered by the existing channel suites.Why
The pairing token currently does four jobs at once: it is the pairing secret, the standing credential, the device's identity and its authority. Device keys (#27493) separated the credential. Tickets now separate the pairing secret.
--ttlwould hide the limit from scripts.Linked Issue
Part of #17397, #17398 and #17400 (host side). They close once a client redeems tickets and the desktop approval dialog ships.
Visual Proof
N/A for this PR: no UI yet. The approval dialog lands in the follow-up. It will show the host name, what the device will be allowed to do, the device's label and key fingerprint, and the 6-character code, with Allow and Deny. When the new device would replace a same-named one, it adds a field where the owner types the code.
Testing
pairing-ticket-book.test.ts, written first:rpc/pairing-ticket-redemption.test.ts: end to end through the real socket wiring with real v2 crypto.ticket_consumed.ticket_consumedwhile the owner decides.pairing-comparison-code.test.ts: a pinned vector that the host (node:crypto) and client (pure JS) both produce.pairing-ticket-offer.test.ts: round trip, only the verified https link, a ticket inside anorca://URL is refused, and the v2 parser rejects it.rpc/pairing-ticket-methods.test.ts: the owner can mint, list and revoke, and the secret never appears in the list. Paired runtime, phone and delegated callers getforbiddeneven if an owner context leaked.src/cli/index-pairing-commands.test.ts:--ttl 2his refused before contacting the runtime;Cross-version (
tests/e2e/cross-version-wire/cross-version-pairing-ticket.unit.test.ts, against the latest release):pairing.ticket.v1;Existing suites: all of
src/main/runtime/rpc,runtime-rpc, device-key, pairing, CLI, and the B3/B2 cross-version tests pass.pnpm tc, oxlint, andpnpm run check:code-quality:changed(0 findings) pass.pnpm lintpasses except the layout-writer ratchet, which fails identically on the base branch for two untouchedsrc/main/ipc/ptyfiles.Automated tests added/updated
Agent skill upstream boundary
Notes
orca pairing createruns on the host it pairs with (remote selection is refused). No desktop UI means no approver, which is why headless tickets don't need one.pairing.ticket.v1. The v3 offer is rejected by older parsers. Redeemed rows are top-level optional fields that an older loader keeps but cannot use.ticketfield in serve readiness;Checklist
N/Afor visuals, with reasonHandoff status (2026-10-11)
264356319fat handoff.redeemproof and durable row write; sealed ticket verdicts; owner-onlyorca pairing create|list|revoke; the link is the verified https link only (neverorca://); optionalephemeral: trueon redemption; capabilitypairing.ticket.v1.orca://secret link, a stuck mid-redemption ticket, reserved-id verdicts, stored-name sanitizing). No open P0/P1 findings.ticketfield; CLI and phone redemption (those clients need B5/B6 keys first); the QR still hands out the legacy offer on purpose, so old phones keep working.orca pairing createprints a link that no client can redeem yet.