ext: pairing-restart test + AMO permission justification

tests/transport/storage.test.ts covers the storage half of "the pairing
token survives a browser restart" (round-trip, unpair-clears, corrupted
override falls back to auto). websocket.test.ts adds the transport half:
a fresh WebSocketTransport instance over the same backing store reuses
the persisted token with no re-pairing, plus pairWithCode/unpair
coverage. "Wrong token rejected and rate-limited" was already covered
(websocket.test.ts's NotPaired/RateLimited cases).

docs/amo-permissions.md is the submission-ready permission justification
for AMO's Notes to Reviewer field, covering every permission in
manifest.json plus what was deliberately not requested and how cookie/
header data is handled.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Ed8KEmAW48v4YHdxLtqsMB
This commit is contained in:
2026-09-11 12:25:42 +04:00
co-authored by Claude Sonnet 5
parent 2cb1959bff
commit 25c171f742
5 changed files with 190 additions and 0 deletions
@@ -62,6 +62,11 @@ export interface VeloxTransport {
/** Fires on every state change. Returns an unsubscribe. */
onStateChange(cb: (status: TransportStatus) => void): () => void;
/** WebSocket transport only (pairing has no meaning over native messaging's uds
* socket, which has no token). Options renders these controls only when present. */
pairWithCode?(code: string): Promise<void>;
unpair?(): Promise<void>;
}
// --- errors ---------------------------------------------------------------------------
@@ -97,6 +97,9 @@ export class WebSocketTransport implements VeloxTransport {
private stopped = false;
private connectPromise: Promise<void> | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
/** Set only for the duration of pairWithCode(); consumed by pair(). docs/05 §4: "the
* user clicks Allow (or types the code in the extension options)." */
private pendingPairCode: string | null = null;
private readonly listeners = new Map<string, Set<EventListener>>();
private readonly stateListeners = new Set<(status: TransportStatus) => void>();
@@ -139,6 +142,29 @@ export class WebSocketTransport implements VeloxTransport {
return this.connectPromise;
}
/**
* Options → "Pair" with a code typed from the daemon's dialog, for when the GUI isn't
* running to click Allow (docs/05 §4). Drops any stored token first so the handshake
* takes the pairing branch, then reconnects with the code attached.
*/
async pairWithCode(code: string): Promise<void> {
this.disconnect();
await this.deps.setToken(null); // drop any stale token so the handshake takes the pairing branch
this.pendingPairCode = code;
try {
await this.connect();
} finally {
this.pendingPairCode = null;
}
}
/** Options → "Unpair": revoke the local token. The daemon's own record of it is
* cleaned up on its side; this only ever forgets our copy. */
async unpair(): Promise<void> {
await this.deps.setToken(null);
this.disconnect();
}
disconnect(): void {
this.stopped = true;
if (this.reconnectTimer) {
@@ -322,6 +348,7 @@ export class WebSocketTransport implements VeloxTransport {
const params: SessionPairParams = {
clientName: this.clientName,
extensionId: this.deps.extensionId,
code: this.pendingPairCode,
};
try {
const res = (await rpc.request(