feat: pairing v2 — one-tap trusted devices over LAN and private relay (#2103)
Reworks how devices connect to an OpenChamber server, end to end. Pairing v2: - One-time pairing links/QR codes (openchamber://connect?v=2) carrying a set of transport candidates (LAN/tunnel/relay) and a single-use secret redeemed server-side; no tokens embedded in links - Add-a-device dialog written for first-time users: intent-based transport choice (Anywhere / Home network only / This computer only) with plain-language descriptions, transparent fallback checkboxes, server-authoritative LAN detection, high-res QR dialog - Private relay folded into pairing as a transport candidate with a demand-driven lifecycle (enables when a relay device is paired, disables when none remain) Multi-transport devices: - A saved device holds all its transports and one token; mobile re-probes on connect, resume, and network change and hot-switches LAN<->relay seamlessly (no re-pairing, no remount, session preserved) - Desktop can import relay pairing links, switch to relay hosts through the E2EE tunnel, and restore a relay default host after relaunch Device management: - Device list (web + desktop) shows live per-device connectivity with the active transport (Connected - Local network / Relay) and platform badges (iOS/Android/macOS/Windows/Linux) - One physical device = one record: stable per-install dedupe keys across pairing and password re-login; typed pairing label names the device, paired devices name the connection by the issuing server hostname - Trusted desktop-local client manages all devices (list, revoke, clear revoked); relay host reaps dead client sockets after 3 missed keepalives Android: - LAN transport unblocked (cleartext + mixed content, mirroring iOS ATS exceptions); resume re-probe retries through network flux and silently auto-reconnects from a disconnected state
This commit is contained in:
@@ -0,0 +1,948 @@
|
||||
# Pairing v2 Trusted-Device Issuance Backend Plan
|
||||
|
||||
## Scope
|
||||
|
||||
Implement the Pairing v2 mechanism without UI.
|
||||
|
||||
Included:
|
||||
|
||||
- Backend pairing session runtime.
|
||||
- Pairing create/redeem/cancel routes.
|
||||
- Trusted-device token issuance through the existing remote client auth runtime.
|
||||
- Backward-compatible remote client metadata extension.
|
||||
- Password/passkey issuance metadata alignment.
|
||||
- Shared v2 `openchamber://connect` payload helpers.
|
||||
|
||||
Not included:
|
||||
|
||||
- Settings page.
|
||||
- QR modal.
|
||||
- Pair Device button.
|
||||
- Device list UI.
|
||||
- Translations/copy.
|
||||
- Relay implementation.
|
||||
- LAN discovery.
|
||||
- End-user polished mobile/desktop screens.
|
||||
|
||||
## Naming
|
||||
|
||||
Use the existing `client-auth` domain.
|
||||
|
||||
New module:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/pairing.js
|
||||
```
|
||||
|
||||
Existing durable token module remains:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/remote-clients.js
|
||||
```
|
||||
|
||||
Conceptual names:
|
||||
|
||||
```text
|
||||
Remote client
|
||||
Trusted-device client token
|
||||
Pairing session
|
||||
Pairing secret
|
||||
Pairing redeem
|
||||
```
|
||||
|
||||
Deep link stays:
|
||||
|
||||
```text
|
||||
openchamber://connect
|
||||
```
|
||||
|
||||
Versions:
|
||||
|
||||
```text
|
||||
v=1 => legacy server + long-lived token import
|
||||
v=2 => one-time pairing handshake
|
||||
```
|
||||
|
||||
## New Files
|
||||
|
||||
### 1. `packages/web/server/lib/client-auth/pairing.js`
|
||||
|
||||
Create a new backend runtime module for short-lived pairing sessions.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
```text
|
||||
createPairingSession
|
||||
getPairingSession
|
||||
cancelPairingSession
|
||||
redeemPairingSession
|
||||
sweepExpiredSessions
|
||||
```
|
||||
|
||||
Store file:
|
||||
|
||||
```text
|
||||
OPENCHAMBER_DATA_DIR/client-pairing-sessions.json
|
||||
```
|
||||
|
||||
Suggested store shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"sessions": [
|
||||
{
|
||||
"id": "pair_...",
|
||||
"secretHash": "...",
|
||||
"createdAt": "...",
|
||||
"expiresAt": "...",
|
||||
"usedAt": null,
|
||||
"cancelledAt": null,
|
||||
"clientId": null,
|
||||
"label": "Pair new device",
|
||||
"fingerprint": "ABCD-1234",
|
||||
"allowedClientKinds": ["mobile", "desktop"],
|
||||
"createdByClientId": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Security requirements:
|
||||
|
||||
```text
|
||||
Persist only secretHash.
|
||||
Return plaintext secret only from createPairingSession.
|
||||
Redeem is one-time.
|
||||
Redeem is expiry-aware.
|
||||
Redeem is cancellation-aware.
|
||||
Redeem must be mutation-serialized to avoid double issuance.
|
||||
No raw token/secret logging.
|
||||
```
|
||||
|
||||
Public methods should accept injected dependencies, following `remote-clients.js` style:
|
||||
|
||||
```js
|
||||
createClientPairingRuntime({
|
||||
fsPromises,
|
||||
path,
|
||||
crypto,
|
||||
storePath,
|
||||
remoteClientAuthRuntime,
|
||||
})
|
||||
```
|
||||
|
||||
## Existing Files To Update
|
||||
|
||||
### 2. `packages/web/server/lib/client-auth/remote-clients.js`
|
||||
|
||||
Extend trusted-device metadata backward-compatibly.
|
||||
|
||||
Current `createClient` input:
|
||||
|
||||
```js
|
||||
{
|
||||
label,
|
||||
expiresAt,
|
||||
clientKind,
|
||||
dedupeKey,
|
||||
}
|
||||
```
|
||||
|
||||
Extend to:
|
||||
|
||||
```js
|
||||
{
|
||||
label,
|
||||
expiresAt,
|
||||
clientKind,
|
||||
dedupeKey,
|
||||
authMethod,
|
||||
pairingId,
|
||||
deviceName,
|
||||
devicePlatform,
|
||||
deviceModel,
|
||||
appVersion,
|
||||
}
|
||||
```
|
||||
|
||||
Add normalized public fields:
|
||||
|
||||
```text
|
||||
authMethod
|
||||
pairingId
|
||||
deviceName
|
||||
devicePlatform
|
||||
deviceModel
|
||||
appVersion
|
||||
```
|
||||
|
||||
Backward compatibility rules:
|
||||
|
||||
```text
|
||||
Existing remote-clients.json remains valid.
|
||||
Missing new fields normalize to null.
|
||||
Existing tokens continue authenticating.
|
||||
Public client output never exposes tokenHash.
|
||||
Raw token is returned only from createClient.
|
||||
```
|
||||
|
||||
Recommended `authMethod` values:
|
||||
|
||||
```text
|
||||
pairing
|
||||
password
|
||||
passkey
|
||||
desktop-local
|
||||
manual
|
||||
legacy
|
||||
```
|
||||
|
||||
Do not force migration for old records. Treat missing `authMethod` as legacy/null.
|
||||
|
||||
### 3. `packages/web/server/index.js`
|
||||
|
||||
Instantiate the new pairing runtime next to `remoteClientAuthRuntime`.
|
||||
|
||||
Existing:
|
||||
|
||||
```js
|
||||
const remoteClientAuthRuntime = createRemoteClientAuthRuntime({
|
||||
fsPromises,
|
||||
path,
|
||||
crypto,
|
||||
storePath: REMOTE_CLIENTS_FILE_PATH,
|
||||
});
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```js
|
||||
const CLIENT_PAIRING_SESSIONS_FILE_PATH = path.join(
|
||||
OPENCHAMBER_DATA_DIR,
|
||||
'client-pairing-sessions.json',
|
||||
);
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```js
|
||||
const clientPairingRuntime = createClientPairingRuntime({
|
||||
fsPromises,
|
||||
path,
|
||||
crypto,
|
||||
storePath: CLIENT_PAIRING_SESSIONS_FILE_PATH,
|
||||
remoteClientAuthRuntime,
|
||||
});
|
||||
```
|
||||
|
||||
Pass `clientPairingRuntime` into `registerAuthAndAccessRoutes` dependencies.
|
||||
|
||||
### 4. `packages/web/server/lib/opencode/core-routes.js`
|
||||
|
||||
Add pairing routes near existing client-auth routes:
|
||||
|
||||
```text
|
||||
/api/client-auth/clients
|
||||
```
|
||||
|
||||
Add:
|
||||
|
||||
```http
|
||||
POST /api/client-auth/pairing/sessions
|
||||
DELETE /api/client-auth/pairing/sessions/:id
|
||||
POST /api/client-auth/pairing/redeem
|
||||
```
|
||||
|
||||
Optional, can be deferred:
|
||||
|
||||
```http
|
||||
GET /api/client-auth/pairing/sessions/:id
|
||||
```
|
||||
|
||||
Since UI polling is out of scope, `GET` is not required for this phase.
|
||||
|
||||
#### Route: `POST /api/client-auth/pairing/sessions`
|
||||
|
||||
Purpose:
|
||||
|
||||
```text
|
||||
Create one short-lived pairing session and return data needed to build QR/deep link.
|
||||
```
|
||||
|
||||
Auth:
|
||||
|
||||
```text
|
||||
Require UI session auth.
|
||||
Allow desktop-local client only if consistent with existing client-create exception.
|
||||
Reject arbitrary remote client tokens.
|
||||
Reject url-token auth.
|
||||
```
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"label": "Pair new device",
|
||||
"allowedClientKinds": ["mobile", "desktop"]
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"pairing": {
|
||||
"id": "pair_...",
|
||||
"secret": "one_time_secret",
|
||||
"expiresAt": "...",
|
||||
"fingerprint": "ABCD-1234",
|
||||
"label": "Pair new device"
|
||||
},
|
||||
"server": {
|
||||
"label": "OpenChamber",
|
||||
"candidates": [
|
||||
{
|
||||
"type": "lan",
|
||||
"url": "http://192.168.1.20:4096",
|
||||
"priority": 10
|
||||
},
|
||||
{
|
||||
"type": "tunnel",
|
||||
"url": "https://abc.ngrok.app",
|
||||
"priority": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Headers:
|
||||
|
||||
```http
|
||||
Cache-Control: no-store
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
```text
|
||||
This route does not render QR.
|
||||
UI can later encode the returned data into openchamber://connect?v=2&p=...
|
||||
```
|
||||
|
||||
#### Route: `DELETE /api/client-auth/pairing/sessions/:id`
|
||||
|
||||
Purpose:
|
||||
|
||||
```text
|
||||
Cancel an unused pairing session.
|
||||
```
|
||||
|
||||
Auth:
|
||||
|
||||
```text
|
||||
Require owner/session auth.
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
```text
|
||||
Set cancelledAt.
|
||||
Do not delete immediately.
|
||||
If already used, cancellation should not revoke the issued client.
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"cancelled": true
|
||||
}
|
||||
```
|
||||
|
||||
#### Route: `POST /api/client-auth/pairing/redeem`
|
||||
|
||||
Purpose:
|
||||
|
||||
```text
|
||||
Exchange pairingId + one-time secret for a trusted-device client token.
|
||||
```
|
||||
|
||||
Auth:
|
||||
|
||||
```text
|
||||
No existing auth required.
|
||||
The one-time pairing secret is the authentication factor.
|
||||
```
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{
|
||||
"pairingId": "pair_...",
|
||||
"secret": "one_time_secret",
|
||||
"clientLabel": "Iryna iPhone",
|
||||
"clientKind": "mobile",
|
||||
"deviceName": "Iryna iPhone",
|
||||
"devicePlatform": "ios",
|
||||
"deviceModel": "iPhone",
|
||||
"appVersion": "1.12.0",
|
||||
"dedupeKey": "optional-stable-device-key"
|
||||
}
|
||||
```
|
||||
|
||||
Server behavior:
|
||||
|
||||
```text
|
||||
Validate pairing exists.
|
||||
Validate secret using constant-time comparison.
|
||||
Validate not expired.
|
||||
Validate not cancelled.
|
||||
Validate not used.
|
||||
Validate clientKind is allowed.
|
||||
Mark pairing used.
|
||||
Create remote client through remoteClientAuthRuntime.createClient.
|
||||
Return clientToken once.
|
||||
```
|
||||
|
||||
Create client with:
|
||||
|
||||
```js
|
||||
{
|
||||
label: clientLabel || deviceName || 'Remote client',
|
||||
clientKind,
|
||||
dedupeKey,
|
||||
authMethod: 'pairing',
|
||||
pairingId,
|
||||
deviceName,
|
||||
devicePlatform,
|
||||
deviceModel,
|
||||
appVersion,
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"server": {
|
||||
"label": "OpenChamber",
|
||||
"url": "https://selected-or-current-url",
|
||||
"fingerprint": "ABCD-1234"
|
||||
},
|
||||
"client": {
|
||||
"id": "device_...",
|
||||
"label": "Iryna iPhone",
|
||||
"clientKind": "mobile",
|
||||
"authMethod": "pairing",
|
||||
"createdAt": "..."
|
||||
},
|
||||
"clientToken": "oc_client_..."
|
||||
}
|
||||
```
|
||||
|
||||
Headers:
|
||||
|
||||
```http
|
||||
Cache-Control: no-store
|
||||
```
|
||||
|
||||
Failure response should be generic:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Invalid or expired pairing session"
|
||||
}
|
||||
```
|
||||
|
||||
Do not reveal whether id, secret, expiry, used, or cancellation caused failure.
|
||||
|
||||
### 5. `packages/web/server/lib/ui-auth/ui-auth.js`
|
||||
|
||||
Preserve existing password/passkey behavior.
|
||||
|
||||
Only add metadata to client token issuance when `issueClientToken === true`.
|
||||
|
||||
Password issuance should pass:
|
||||
|
||||
```js
|
||||
authMethod: 'password'
|
||||
clientKind: req.body?.clientKind
|
||||
dedupeKey: req.body?.dedupeKey
|
||||
deviceName: req.body?.deviceName
|
||||
devicePlatform: req.body?.devicePlatform
|
||||
deviceModel: req.body?.deviceModel
|
||||
appVersion: req.body?.appVersion
|
||||
```
|
||||
|
||||
Passkey issuance should pass:
|
||||
|
||||
```js
|
||||
authMethod: 'passkey'
|
||||
clientKind: req.body?.clientKind
|
||||
dedupeKey: req.body?.dedupeKey
|
||||
deviceName: req.body?.deviceName
|
||||
devicePlatform: req.body?.devicePlatform
|
||||
deviceModel: req.body?.deviceModel
|
||||
appVersion: req.body?.appVersion
|
||||
```
|
||||
|
||||
Backward compatibility:
|
||||
|
||||
```text
|
||||
Existing POST /auth/session payload still works.
|
||||
Existing response shape still works.
|
||||
Existing clientToken issuance still works.
|
||||
Password login remains disabled for tunnel/public scope.
|
||||
```
|
||||
|
||||
### 6. `packages/ui/src/lib/connectionPayload.ts`
|
||||
|
||||
Extend existing connect payload helpers.
|
||||
|
||||
Keep current v1 behavior:
|
||||
|
||||
```text
|
||||
openchamber://connect?v=1&server=...&token=...&label=...
|
||||
```
|
||||
|
||||
Add v2 payload types and helpers.
|
||||
|
||||
Suggested types:
|
||||
|
||||
```ts
|
||||
export type ClientConnectionPayloadV1 = {
|
||||
v: 1;
|
||||
serverUrl: string;
|
||||
token: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type PairingEndpointCandidate = {
|
||||
type: 'lan' | 'tunnel' | 'relay';
|
||||
url: string;
|
||||
priority?: number;
|
||||
};
|
||||
|
||||
export type PairingConnectionPayloadV2 = {
|
||||
v: 2;
|
||||
pairingId: string;
|
||||
secret: string;
|
||||
label?: string;
|
||||
fingerprint?: string;
|
||||
expiresAt?: string;
|
||||
candidates: PairingEndpointCandidate[];
|
||||
};
|
||||
```
|
||||
|
||||
Suggested helpers:
|
||||
|
||||
```ts
|
||||
encodePairingConnectionPayload(payload: PairingConnectionPayloadV2): string
|
||||
parsePairingConnectionPayload(value: string): PairingConnectionPayloadV2 | null
|
||||
```
|
||||
|
||||
Use deep link format:
|
||||
|
||||
```text
|
||||
openchamber://connect?v=2&p=<base64url-json>
|
||||
```
|
||||
|
||||
Validation:
|
||||
|
||||
```text
|
||||
Require v=2.
|
||||
Require pairingId.
|
||||
Require secret.
|
||||
Require at least one valid http/https candidate.
|
||||
Reject malformed URL.
|
||||
Reject oversized payload.
|
||||
Reject expired payload locally if expiresAt is clearly in the past.
|
||||
```
|
||||
|
||||
Do not break current exports used by mobile QR/manual connect.
|
||||
|
||||
### 7. `packages/ui/src/apps/mobileQrScan.ts`
|
||||
|
||||
Update parser only.
|
||||
|
||||
Current scan parser recognizes legacy fields like:
|
||||
|
||||
```text
|
||||
server
|
||||
label
|
||||
```
|
||||
|
||||
Add support for v2 connect links.
|
||||
|
||||
Output should be able to distinguish:
|
||||
|
||||
```text
|
||||
legacy v1 token import
|
||||
pairing v2 payload
|
||||
plain URL
|
||||
```
|
||||
|
||||
Do not implement full mobile UI flow in this scope unless there is already a non-UI callable path.
|
||||
|
||||
### 8. `packages/ui/src/apps/mobileConnections.ts`
|
||||
|
||||
Add non-visual callable mechanism for redeeming pairing payload.
|
||||
|
||||
Add a function conceptually like:
|
||||
|
||||
```ts
|
||||
redeemPairingConnection(payload: PairingConnectionPayloadV2): Promise<void>
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
```text
|
||||
Try endpoint candidates.
|
||||
POST /api/client-auth/pairing/redeem.
|
||||
Persist issued token securely.
|
||||
Persist connection metadata.
|
||||
Switch runtime only after token write succeeds.
|
||||
```
|
||||
|
||||
No new screens/buttons.
|
||||
|
||||
Existing password flow remains unchanged.
|
||||
|
||||
Candidate selection:
|
||||
|
||||
```text
|
||||
Normalize candidates.
|
||||
Probe /health with timeout.
|
||||
Try candidates by priority.
|
||||
Prefer HTTPS when priority ties.
|
||||
If network failure, try next candidate.
|
||||
If server says invalid/expired/used, stop.
|
||||
```
|
||||
|
||||
Mobile native should reuse existing native HTTP fallback path for LAN HTTP.
|
||||
|
||||
### 9. `packages/electron/main.mjs`
|
||||
|
||||
Extend existing connect deep-link handling.
|
||||
|
||||
Current v1 behavior:
|
||||
|
||||
```text
|
||||
openchamber://connect?v=1&server=...&token=...
|
||||
```
|
||||
|
||||
Keep it.
|
||||
|
||||
Add v2 branch:
|
||||
|
||||
```text
|
||||
openchamber://connect?v=2&p=...
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
```text
|
||||
Parse v2 payload.
|
||||
Show confirmation before redeem/write/switch.
|
||||
Probe candidates.
|
||||
Redeem pairing secret.
|
||||
Store returned clientToken in desktop hosts config.
|
||||
Ask/switch according to existing remote host behavior.
|
||||
Never show token.
|
||||
Never write config before confirmation.
|
||||
```
|
||||
|
||||
If this phase is strictly backend-only, this file can be deferred. But if desktop app as client must be functionally supported by deep link in this phase, include this change.
|
||||
|
||||
### 10. `packages/electron/preload.mjs`
|
||||
|
||||
No change expected unless a renderer-side desktop API is needed for pairing redeem.
|
||||
|
||||
Prefer keeping pairing redeem in main process only for deep-link handling if desktop v2 is implemented there.
|
||||
|
||||
### 11. `packages/web/server/lib/ui-auth/DOCUMENTATION.md`
|
||||
|
||||
Update module documentation to reflect the unified issuance model:
|
||||
|
||||
```text
|
||||
Password, passkey, and pairing are issuance methods.
|
||||
Trusted-device client token is the durable credential.
|
||||
Pairing v2 uses one-time secrets and issues remote client tokens.
|
||||
```
|
||||
|
||||
Optionally add:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/DOCUMENTATION.md
|
||||
```
|
||||
|
||||
if the client-auth module needs ownership docs.
|
||||
|
||||
## Route Registration Summary
|
||||
|
||||
Add to `registerAuthAndAccessRoutes`:
|
||||
|
||||
```http
|
||||
POST /api/client-auth/pairing/sessions
|
||||
DELETE /api/client-auth/pairing/sessions/:id
|
||||
POST /api/client-auth/pairing/redeem
|
||||
```
|
||||
|
||||
Optional later:
|
||||
|
||||
```http
|
||||
GET /api/client-auth/pairing/sessions/:id
|
||||
```
|
||||
|
||||
Route placement:
|
||||
|
||||
```text
|
||||
Register before generic OpenCode proxy.
|
||||
Place near existing /api/client-auth/clients routes.
|
||||
```
|
||||
|
||||
## Execution Sequence
|
||||
|
||||
### Step 1: Extend Remote Client Metadata
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/remote-clients.js
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Add metadata normalization.
|
||||
Extend createClient input.
|
||||
Extend publicClient output.
|
||||
Keep old records valid.
|
||||
Do not change token generation/authentication behavior.
|
||||
```
|
||||
|
||||
### Step 2: Add Password/Passkey Metadata Issuance
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/ui-auth/ui-auth.js
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
When issueClientToken is true, pass authMethod='password' from password login.
|
||||
When issueClientToken is true, pass authMethod='passkey' from passkey auth.
|
||||
Pass optional device metadata through.
|
||||
Preserve response shape.
|
||||
```
|
||||
|
||||
### Step 3: Create Pairing Runtime Module
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/pairing.js
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Implement session creation.
|
||||
Implement hashed secret storage.
|
||||
Implement cancel.
|
||||
Implement redeem.
|
||||
Implement expiry/used/cancelled checks.
|
||||
Integrate remoteClientAuthRuntime.createClient in redeem.
|
||||
```
|
||||
|
||||
### Step 4: Instantiate Pairing Runtime
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/index.js
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Define CLIENT_PAIRING_SESSIONS_FILE_PATH.
|
||||
Instantiate createClientPairingRuntime.
|
||||
Pass clientPairingRuntime to registerAuthAndAccessRoutes.
|
||||
```
|
||||
|
||||
### Step 5: Add Pairing Routes
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/opencode/core-routes.js
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Destructure clientPairingRuntime from dependencies.
|
||||
Add POST /api/client-auth/pairing/sessions.
|
||||
Add DELETE /api/client-auth/pairing/sessions/:id.
|
||||
Add POST /api/client-auth/pairing/redeem.
|
||||
Use correct auth gates.
|
||||
Set Cache-Control: no-store where secrets/tokens are returned.
|
||||
Keep error responses generic for redeem.
|
||||
```
|
||||
|
||||
### Step 6: Add v2 Payload Helpers
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/ui/src/lib/connectionPayload.ts
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Keep v1 helpers unchanged.
|
||||
Add v2 payload type.
|
||||
Add encode v2 helper.
|
||||
Add parse v2 helper.
|
||||
Use openchamber://connect?v=2&p=<base64url-json>.
|
||||
Validate candidates.
|
||||
Reject malformed/expired/oversized payloads.
|
||||
```
|
||||
|
||||
### Step 7: Update QR Scan Parser Shape
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/ui/src/apps/mobileQrScan.ts
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Recognize v2 connect payload.
|
||||
Return structured v2 result.
|
||||
Do not add new UI.
|
||||
Do not break v1/manual URL behavior.
|
||||
```
|
||||
|
||||
### Step 8: Add Non-UI Mobile Redeem Plumbing
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/ui/src/apps/mobileConnections.ts
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Add callable redeem pairing function.
|
||||
Try endpoint candidates.
|
||||
Redeem via /api/client-auth/pairing/redeem.
|
||||
Persist token before runtime switch.
|
||||
Reuse existing storage model.
|
||||
Keep password/manual connect unchanged.
|
||||
```
|
||||
|
||||
### Step 9: Add Desktop Deep-Link v2 Handling If In Scope
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/electron/main.mjs
|
||||
```
|
||||
|
||||
Do:
|
||||
|
||||
```text
|
||||
Extend connect deep-link parser to recognize v2.
|
||||
Confirm before redeem.
|
||||
Redeem against candidate endpoint.
|
||||
Store remote host config with returned token.
|
||||
Switch only after confirmation and successful storage.
|
||||
Keep v1 behavior unchanged.
|
||||
```
|
||||
|
||||
If desktop client deep-link support is deferred, skip this step and document that v2 backend/shared payload exists but desktop consumer is not wired yet.
|
||||
|
||||
### Step 10: Update Documentation
|
||||
|
||||
Files:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/ui-auth/DOCUMENTATION.md
|
||||
```
|
||||
|
||||
Optionally add:
|
||||
|
||||
```text
|
||||
packages/web/server/lib/client-auth/DOCUMENTATION.md
|
||||
```
|
||||
|
||||
Document:
|
||||
|
||||
```text
|
||||
Unified trusted-device token issuance.
|
||||
Pairing v2 flow.
|
||||
Password/passkey/pairing authMethod values.
|
||||
Security rules.
|
||||
Backward compatibility guarantees.
|
||||
```
|
||||
|
||||
## Important Non-Goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
```text
|
||||
Settings page
|
||||
Pair Device button
|
||||
QR modal
|
||||
Device list UI
|
||||
Translations
|
||||
Visual design
|
||||
Relay transport
|
||||
LAN discovery
|
||||
Account/cloud sync
|
||||
Token migration to OS keychain on desktop
|
||||
```
|
||||
|
||||
## Backward Compatibility Requirements
|
||||
|
||||
Must remain true:
|
||||
|
||||
```text
|
||||
Existing v1 openchamber://connect links keep working.
|
||||
Existing password login with issueClientToken keeps working.
|
||||
Existing passkey issueClientToken keeps working.
|
||||
Existing remote-clients.json keeps loading.
|
||||
Existing client tokens keep authenticating.
|
||||
Existing mobile saved connections keep working.
|
||||
Existing desktop remote hosts keep working.
|
||||
```
|
||||
|
||||
## Security Requirements
|
||||
|
||||
Must hold:
|
||||
|
||||
```text
|
||||
No long-lived token in v2 link.
|
||||
Pairing secret persisted only as hash.
|
||||
Pairing secret returned only once.
|
||||
Client token returned only once.
|
||||
Token hash persisted server-side.
|
||||
Redeem is one-time.
|
||||
Redeem is expiry-aware.
|
||||
Redeem is cancellation-aware.
|
||||
Redeem errors are generic.
|
||||
Password login remains disabled for tunnel/public scope.
|
||||
Pairing session creation requires owner/session auth.
|
||||
Pairing redeem requires no prior auth but requires valid one-time secret.
|
||||
Desktop v2 connect confirms before writing host config or switching runtime.
|
||||
```
|
||||
Reference in New Issue
Block a user