@agentproto/pairing-host 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -127,6 +127,8 @@ function createPairingRegistry(deps) {
127
127
  const handshakeTimeoutMs = deps.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
128
128
  const revokedGraceMs = deps.revokedGraceMs ?? DEFAULT_REVOKED_GRACE_MS;
129
129
  const dialFailureGate = createReconnectLogGate({ now });
130
+ const handshakeFailureGate = createReconnectLogGate({ now });
131
+ const handshakeCloseGate = createReconnectLogGate({ now });
130
132
  const pairings = /* @__PURE__ */ new Map();
131
133
  const onlineCounts = /* @__PURE__ */ new Map();
132
134
  const revoked = /* @__PURE__ */ new Map();
@@ -318,15 +320,32 @@ function createPairingRegistry(deps) {
318
320
  capturedHello = hello;
319
321
  return { reply: encodePairingMessage(result.reply), keys: result.session };
320
322
  },
321
- { timeoutMs: handshakeTimeoutMs }
323
+ {
324
+ timeoutMs: handshakeTimeoutMs,
325
+ // Post-handshake close reason (BOOTSTRAP P4 item 2, field
326
+ // evidence: the flaps break AFTER the reply is sent, and the
327
+ // reason was captured at this exact layer and discarded).
328
+ // Gated like the handshake-failure log so a flap's retry loop
329
+ // doesn't bury daemon.log.
330
+ log: (line) => {
331
+ const gated = handshakeCloseGate.onFailure(spec.key, `[pairing] ${spec.key}: ${line}`);
332
+ if (gated) log(gated);
333
+ }
334
+ }
322
335
  );
323
336
  } catch (err) {
324
337
  if (signal.aborted) break;
325
- void err;
338
+ const line = handshakeFailureGate.onFailure(
339
+ spec.key,
340
+ `[pairing] handshake for ${spec.key} failed: ${errMsg(err)}`
341
+ );
342
+ if (line) log(line);
326
343
  await sleep(backoff, signal);
327
344
  backoff = Math.min(backoff * 2, reconnectMaxMs);
328
345
  continue;
329
346
  }
347
+ handshakeFailureGate.onSuccess(spec.key);
348
+ handshakeCloseGate.onSuccess(spec.key);
330
349
  if (legacyPeer) {
331
350
  sendOutdatedNotice(wrapped);
332
351
  log(`[pairing] told a pair/v1 client on ${spec.key} to re-pair`);
@@ -358,14 +377,16 @@ function createPairingRegistry(deps) {
358
377
  channels.add(handle);
359
378
  onlineCounts.set(ctx.fingerprint, (onlineCounts.get(ctx.fingerprint) ?? 0) + 1);
360
379
  log(`[pairing] channel up (${ctx.mode}) for ${ctx.fingerprint} via ${spec.key}`);
361
- await waitClosed(wrapped, signal);
380
+ const closeReason = await waitClosed(wrapped, signal);
362
381
  channels.delete(handle);
363
382
  await handle.close().catch(() => {
364
383
  });
365
384
  const remaining = (onlineCounts.get(ctx.fingerprint) ?? 1) - 1;
366
385
  if (remaining > 0) onlineCounts.set(ctx.fingerprint, remaining);
367
386
  else onlineCounts.delete(ctx.fingerprint);
368
- log(`[pairing] channel closed (${ctx.mode}) for ${ctx.fingerprint}`);
387
+ log(
388
+ `[pairing] channel closed (${ctx.mode}) for ${ctx.fingerprint}${closeReason !== void 0 ? `: ${closeReason}` : " (remote closed without a reason)"}`
389
+ );
369
390
  if (spec.singleUse) break;
370
391
  } finally {
371
392
  signal.removeEventListener("abort", onAbort);
@@ -688,16 +709,16 @@ var REVOKED = /* @__PURE__ */ Symbol("pairing revoked");
688
709
  function waitClosed(sink, signal) {
689
710
  return new Promise((resolve) => {
690
711
  if (!sink.isOpen) {
691
- resolve();
712
+ resolve(void 0);
692
713
  return;
693
714
  }
694
715
  let settled = false;
695
- const finish = () => {
716
+ const finish = (reason) => {
696
717
  if (settled) return;
697
718
  settled = true;
698
- resolve();
719
+ resolve(reason);
699
720
  };
700
- sink.onClose(() => finish());
721
+ sink.onClose((reason) => finish(reason));
701
722
  signal.addEventListener("abort", () => {
702
723
  if (signal.reason === REVOKED) {
703
724
  sink.send(revokedFrame());
@@ -705,7 +726,7 @@ function waitClosed(sink, signal) {
705
726
  } else {
706
727
  sink.close("registry shutdown");
707
728
  }
708
- finish();
729
+ finish("registry shutdown");
709
730
  });
710
731
  });
711
732
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/reconnect-log-gate.ts","../src/local-device.ts","../src/pairing-registry.ts","../src/dial.ts","../src/loopback-http.ts"],"names":["randomBytes","createHmac","timingSafeEqual"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Rate-limited failure logging for reconnect loops.\n *\n * A standing reconnect loop (the outbound tunnel, a pairing rendezvous) whose\n * peer is permanently gone re-dials on backoff forever. Logging every single\n * failure buries the daemon log — a single dead pairing produced ~85% of one\n * 2.9MB never-rotated `~/.agentproto/daemon.log`, and the tunnel reconnect\n * spam another chunk. This gate logs the FIRST failure per key immediately,\n * then at most one line per `windowMs`, and when it does emit again it carries\n * the count of failures it swallowed in between. A successful connect resets\n * the key so the next outage logs promptly again.\n *\n * Deliberately dependency-free: a per-key `{ lastLoggedAt, suppressed }` map.\n * `now` is injectable so tests can advance the clock without real timers. This\n * only changes logging cadence — reconnect timing/backoff is untouched.\n */\nexport interface ReconnectLogGate {\n /**\n * Record a failure for `key`. Returns the line to log now — the given\n * `message`, with a `(N failures suppressed)` suffix when repeats were\n * swallowed since the last emit — or `null` to stay quiet this time.\n */\n onFailure(key: string, message: string): string | null\n /** Reset suppression for `key` after a successful connect. */\n onSuccess(key: string): void\n}\n\nexport function createReconnectLogGate(\n opts: { windowMs?: number; now?: () => number } = {},\n): ReconnectLogGate {\n const windowMs = opts.windowMs ?? 60_000\n const now = opts.now ?? Date.now\n const state = new Map<string, { lastLoggedAt: number; suppressed: number }>()\n return {\n onFailure(key, message) {\n const ts = now()\n const prev = state.get(key)\n if (prev && ts - prev.lastLoggedAt < windowMs) {\n prev.suppressed += 1\n return null\n }\n const suffix =\n prev && prev.suppressed > 0\n ? ` (${prev.suppressed} failure${prev.suppressed === 1 ? \"\" : \"s\"} suppressed)`\n : \"\"\n state.set(key, { lastLoggedAt: ts, suppressed: 0 })\n return `${message}${suffix}`\n },\n onSuccess(key) {\n state.delete(key)\n },\n }\n}\n","/**\n * Local-device bearer primitives (see \"Local devices\" in pairing-registry.ts).\n * Pure functions; the registry owns storage and file freshness.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\"\n\nexport const LOCAL_BEARER_PREFIX = \"apd1\"\nconst MAC_CONTEXT = \"agentproto/local-device/v1|\"\n\n/** One local device as persisted in `pairings.json` under `localDevices`. */\nexport interface LocalDeviceRecord {\n /** 32 lowercase hex chars: the public handle (also the bearer's lookup key). */\n fingerprint: string\n name: string\n /** ISO-8601. */\n createdAt: string\n /** Per-device secret (base64url, 32 bytes). Never leaves the host. */\n secret: string\n}\n\nexport function newLocalDevice(name: string, createdAt: string): LocalDeviceRecord {\n return {\n fingerprint: randomBytes(16).toString(\"hex\"),\n name,\n createdAt,\n secret: randomBytes(32).toString(\"base64url\"),\n }\n}\n\nfunction macOf(rec: Pick<LocalDeviceRecord, \"fingerprint\" | \"secret\">): Buffer {\n return createHmac(\"sha256\", Buffer.from(rec.secret, \"base64url\"))\n .update(MAC_CONTEXT + rec.fingerprint, \"utf8\")\n .digest()\n}\n\n/** `apd1.<fingerprint>.<mac>`: opaque to the client, re-derivable by the host. */\nexport function deriveLocalBearer(rec: LocalDeviceRecord): string {\n return `${LOCAL_BEARER_PREFIX}.${rec.fingerprint}.${macOf(rec).toString(\"base64url\")}`\n}\n\nexport interface ParsedBearer {\n fingerprint: string\n /** Raw base64url as presented (NOT re-encoded from the decoded buffer — the\n * trailing bits of a 32-byte MAC are dropped by base64url decoding, so the\n * last character must be compared in string form). */\n mac: string\n}\n\nexport function parseLocalBearer(bearer: string): ParsedBearer | null {\n if (typeof bearer !== \"string\" || bearer.length > 256) return null\n const parts = bearer.split(\".\")\n if (parts.length !== 3 || parts[0] !== LOCAL_BEARER_PREFIX) return null\n const [, fingerprint, mac] = parts as [string, string, string]\n if (!/^[0-9a-f]{32}$/.test(fingerprint) || !/^[A-Za-z0-9_-]+$/.test(mac)) return null\n return { fingerprint, mac }\n}\n\n/** Constant-time MAC check. Runs the same HMAC + compare for an unknown\n * fingerprint (against a throwaway secret) so the miss path costs the same. */\nexport function verifyLocalBearer(rec: LocalDeviceRecord | undefined, parsed: ParsedBearer | null): boolean {\n const target = rec ?? { fingerprint: \"0\".repeat(32), secret: randomBytes(32).toString(\"base64url\") }\n const expected = macOf(target).toString(\"base64url\")\n const presented = parsed?.mac ?? \"\"\n const sameLength = presented.length === expected.length\n const equal = timingSafeEqual(\n Buffer.from(expected),\n Buffer.from(sameLength ? presented : \"0\".repeat(expected.length)),\n )\n return Boolean(rec) && parsed !== null && sameLength && equal\n}\n\nexport function isLocalDeviceRecord(v: unknown): v is LocalDeviceRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"fingerprint\"] === \"string\" &&\n /^[0-9a-f]{32}$/.test(r[\"fingerprint\"]) &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"createdAt\"] === \"string\" &&\n typeof r[\"secret\"] === \"string\" &&\n r[\"secret\"].length > 0\n )\n}\n","/**\n * Host-side pairing registry (design: DESIGN §3/§4, PLAN deliverable 2). Lives\n * in `@agentproto/pairing-host` so any AIP-59 host — the agentproto daemon is\n * one, a standalone app is another — can embed it; `@agentproto/runtime`\n * re-exports it unchanged.\n *\n * Owns everything a host needs to be *paired with* over an untrusted\n * rendezvous:\n *\n * - **Offer store** — in-memory, single-use, expiry-checked one-time offer\n * secrets minted by `pair offer`. A daemon restart voids outstanding offers\n * (acceptable + documented). The daemon parks on the offer's ROUTE token and\n * the handshake's `verifyAuthToken` predicate checks the sealed AUTH token,\n * SPENDING the offer on the first successful handshake (a wrong token spends\n * nothing).\n * - **Pairings store** — `~/.agentproto/pairings.json` (0600, atomic write),\n * one record per paired client: `{clientPub, name, fingerprint, createdAt,\n * lastSeen, pairRoot, rendezvousUrl}`. `pairRoot` is the long-term shared\n * secret (`derivePairRoot`) from which reconnect routing tokens derive.\n * - **Rendezvous connections** — for a fresh offer, and for every persisted\n * pairing on boot (autoconnect), the daemon dials the rendezvous *outbound*\n * (`side=daemon&t=<route>`), runs `daemonHandshakeOverSink` with the P1\n * crypto, and — on success — serves the spliced, E2E-wrapped channel exactly\n * like `serve --connect` serves a tunnel host. The serving path itself is\n * injected (`serve`) so this module stays free of pty/adapter concerns and\n * the CLI can reuse its `createTunnelServer` config verbatim.\n * - **Reconnect epochs** — a persisted pairing's standing connection parks on\n * the pairing-derived route `HKDF(pairRoot, \"rv-route\"‖epoch)`, rotated per\n * UTC day. The daemon parks on BOTH the current and previous\n * epoch tokens so a client whose clock straddles midnight still finds it;\n * reconnect-with-backoff keeps the standing connection alive.\n * - **Revocation** — removing a pairing drops its rendezvous connections, so\n * it can no longer be served. For a grace period (`revokedGraceMs`, default\n * 14 days) the daemon keeps a *tombstone*: it still parks on the pairing's\n * epoch ROUTES, completes the (daemon-signed) handshake only for a hello\n * carrying that epoch's AUTH token, and answers with a single E2E-encrypted\n * `error{code:\"pairing_revoked\"}` frame before closing — never serving the\n * channel. That is what lets a client tell \"unpaired\" from \"daemon\n * offline\" (which both otherwise look like a park timeout at the broker)\n * and stop retrying. The signal is authenticated like every tunnel frame:\n * it arrives inside the AEAD channel of a handshake whose transcript the\n * client verified against the pinned daemon key, so the broker can't forge\n * it; and the broker, which knows the routes, can't elicit it either (no\n * auth token). A tombstone keeps only the per-epoch route + auth tokens for\n * its window — the pair root is dropped at revoke.\n *\n * ## Reconnect authentication (why no stable client key)\n *\n * The handshake carries only a client *ephemeral* key (regenerated per\n * handshake) — there is no persistent client identity key. So a reconnect\n * authenticates the client by **possession of the epoch AUTH token**\n * `HKDF(pairRoot, \"rv-auth\"‖epoch)`, which derives from `pairRoot` — a secret\n * only the paired client and daemon share (it fell out of the original ECDH).\n * The daemon parks on the pairing's epoch ROUTE, and on that connection the\n * `verifyAuthToken` predicate accepts a hello whose sealed auth equals the same\n * epoch's auth token (constant-time). The route and auth are separate one-way\n * HKDF outputs: the broker sees every route and can't compute an auth from it.\n * (pair/v1 used ONE value for both, so a broker could replay a logged route as\n * the proof and be served.) The stored `fingerprint` (of the first handshake's\n * ephemeral key) is a stable display/revocation handle, not an auth input.\n *\n * ## Legacy (pair/v1) pairings\n *\n * A v1 `pairings.json` is loaded with every record flagged `legacy`: listed and\n * revocable, never served. The daemon still parks on a legacy pairing's routes\n * (the epoch route is unchanged from v1) so that pairing's not-yet-upgraded\n * client reaches it and is told to re-pair — see `respondToLegacyHandshake` —\n * rather than timing out. v1 records are not upgraded in place: v1 let the\n * broker pair as a client, so any v1 record may be the broker's own.\n */\n\n/*\n * ## Local devices (optional host feature)\n *\n * A loopback client (an MCP host on the same machine) has no use for a\n * rendezvous round-trip, yet should still be \"a paired device\": listed,\n * named, revocable. `mintLocalDevice({name})` creates one without a handshake:\n * - a record `{fingerprint, name, createdAt, secret}` (32 random bytes of\n * per-device `secret`) is appended to the OPTIONAL `localDevices` array of\n * `pairings.json` (still `v: 2`, still 0600; files without the key load\n * unchanged, and an older host that rewrites the file simply drops it,\n * which fails closed: those devices stop verifying).\n * - the bearer is `apd1.<fingerprint>.<HMAC-SHA256(secret, \"…|fingerprint\")>`\n * (base64url). It is never stored or logged; the host re-derives it.\n * - `verifyDeviceBearer(b)` looks the record up by fingerprint, compares the\n * MAC in constant time, and returns `{fingerprint, name}` or null. It stats\n * `pairings.json` on every call and reloads `localDevices` when the file\n * changed, so a `revoke` by another process (a CLI) takes effect on the\n * very next verify; nothing is cached past a revoke.\n * - local devices show in `list()` (flagged `local: true`, with empty\n * `clientPub`/`pairRoot`/`rendezvousUrl`; the secret is never returned),\n * `revoke()` and `rename()` work by fingerprint or name, no tombstone, no\n * rendezvous loop.\n */\n\nimport { randomBytes, createHmac, timingSafeEqual } from \"node:crypto\"\nimport { mkdir, readFile, writeFile, chmod, rename, stat } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { dirname, join, basename } from \"node:path\"\nimport {\n daemonHandshakeOverSink,\n type ErrorFrame,\n type FrameSink,\n type E2eFrameSink,\n} from \"@agentproto/acp/tunnel\"\nimport {\n decodePairingHello,\n encodePairingMessage,\n respondToHandshake,\n respondToLegacyHandshake,\n derivePairRoot,\n deriveEpochTokens,\n deriveOfferTokens,\n currentEpoch,\n encodeOfferUrl,\n HOSTED_RENDEZVOUS_URL,\n OFFER_VERSION,\n PAIRING_PROTOCOL_OUTDATED_MESSAGE,\n PairingError,\n type RouteAuthTokens,\n} from \"@agentproto/secrets/pairing\"\nimport {\n identityFingerprint,\n type DaemonIdentity,\n} from \"@agentproto/secrets/identity\"\nimport { createReconnectLogGate } from \"./reconnect-log-gate.js\"\nimport {\n deriveLocalBearer,\n isLocalDeviceRecord,\n newLocalDevice,\n parseLocalBearer,\n verifyLocalBearer,\n type LocalDeviceRecord,\n} from \"./local-device.js\"\n\n/** `pairings.json` format. v2 = pairings made under pair/v2 (route/auth\n * split). A v1 file still loads, every record flagged `legacy`. */\nexport const PAIRINGS_VERSION = 2 as const\nconst LEGACY_PAIRINGS_VERSION = 1\n\n/** One persisted pairing. Written verbatim to `pairings.json`. */\nexport interface PairingRecord {\n /** The client ephemeral public key from the FIRST handshake (a snapshot —\n * reconnects use fresh ephemerals; this is not an auth input). */\n clientPub: string\n /** Human-facing label the client chose on `pair accept`. */\n name: string\n /** `identityFingerprint(clientPub)` from the first handshake — the stable\n * display + revocation handle. */\n fingerprint: string\n /** ISO-8601 first-pair timestamp. */\n createdAt: string\n /** ISO-8601 of the most recent successful (re)connect. */\n lastSeen: string\n /** Long-term shared secret (base64), from which epoch routing tokens derive. */\n pairRoot: string\n /** Rendezvous endpoint to reconnect through. */\n rendezvousUrl: string\n /** Set on a pairing made under the retired pair/v1 protocol: listed and\n * revocable, never served — its client is told to re-pair. */\n legacy?: true\n /** Set when this pairing was made under an offer minted with\n * `agentproto pair offer --host` (PR-C) — i.e. the offering daemon's own\n * intent, recorded server-side at handshake time from the OfferEntry it\n * itself created, never from the (unauthenticated) `scope` param a client\n * presented in the offer URL. Absent for an ordinary remote-control\n * pairing. Purely informational here — it does not change how this\n * pairing is served. */\n scope?: \"host\"\n /** Set on a record returned by `list()` for a local device (see \"Local\n * devices\"): it has no rendezvous, `clientPub`/`pairRoot`/`rendezvousUrl`\n * are empty strings. Never written to the `pairings` array of the file. */\n local?: true\n}\n\n/** A revoked pairing still answered with `pairing_revoked` until `expiresAt`\n * (see \"Revocation\" above). Persisted in `pairings.json` under `revoked`. */\nexport interface RevokedPairingRecord {\n /** The revoked pairing's fingerprint. */\n fingerprint: string\n /** Its client label. */\n name: string\n /** Rendezvous endpoint its client reconnects through. */\n rendezvousUrl: string\n /** ISO-8601 revocation time. */\n revokedAt: string\n /** ISO-8601 end of the grace window; the tombstone is dropped after it. */\n expiresAt: string\n /** The epoch route (parked on) + auth (verified) tokens covering the window\n * (previous epoch at revoke time through the epoch of `expiresAt`) —\n * derived from the pair root at revoke time so the root itself need not be\n * kept. */\n routes: ({ epoch: number } & RouteAuthTokens)[]\n}\n\ninterface PairingsFile {\n v: typeof PAIRINGS_VERSION | typeof LEGACY_PAIRINGS_VERSION\n pairings: PairingRecord[]\n /** Tombstones of revoked pairings (absent in files written before them;\n * ignored by daemons that predate them). */\n revoked?: RevokedPairingRecord[]\n /** Local-device credentials (see \"Local devices\"). Optional: absent in files\n * written before them. */\n localDevices?: LocalDeviceRecord[]\n}\n\n/** The tunnel `error` code a revoked pairing's client receives. */\nexport const PAIRING_REVOKED_CODE = \"pairing_revoked\" as const\n\nfunction revokedFrame(): ErrorFrame {\n return {\n t: \"error\",\n code: PAIRING_REVOKED_CODE,\n message: \"this device was unpaired from the daemon (agentproto pair revoke); pair again with a new offer\",\n }\n}\n\n/** Mode of a served channel — first-contact offer vs. an established reconnect. */\nexport type PairingChannelMode = \"offer\" | \"reconnect\"\n\n/** Context handed to the injected `serve` when a channel comes up. */\nexport interface PairingChannelContext {\n mode: PairingChannelMode\n /** Stable pairing fingerprint (present for reconnect; for an offer it's the\n * freshly-derived one). */\n fingerprint: string\n /** Client label. */\n name: string\n /** This pairing's OWN recorded scope (mirrors `PairingRecord.scope`) —\n * `\"host\"` when the offer that produced it was minted with\n * `agentproto pair offer --host`, absent for an ordinary remote-control\n * pairing. Present on BOTH the offer and every reconnect context, so\n * `serve` can gate scope-restricted routes (e.g. device-inference\n * proxying) the same way on first contact and on every later reconnect. */\n scope?: \"host\"\n}\n\n/** Handle to a live served channel — the registry closes it on teardown. */\nexport interface PairingChannelHandle {\n close(): Promise<void>\n}\n\nexport interface CreatedOffer {\n /** The one-time offer secret (base64url) — the URL's `s`. Never on the wire:\n * the broker only ever sees its derived route. */\n secret: string\n /** Offer expiry, unix seconds. */\n exp: number\n /** The full `agentproto://pair?…` offer URL. */\n url: string\n /** The daemon's identity fingerprint (shown to the human at accept time). */\n fingerprint: string\n /** The rendezvous the offer routes through. */\n rendezvousUrl: string\n /** True when `rendezvousUrl` is the hosted default — i.e. neither an explicit\n * `--rendezvous` nor a configured `pairing.rendezvous` applied, so the offer\n * falls back to `HOSTED_RENDEZVOUS_URL`. Surfaced so `pair offer` can flag,\n * never silently, that the daemon is relaying through our infrastructure. */\n rendezvousIsHostedDefault: boolean\n /** Echoes `CreateOfferInput.scope`. `\"host\"` when minted with `--host`;\n * absent for a plain offer. */\n scope?: \"host\"\n}\n\nexport interface CreateOfferInput {\n /** Time-to-live in ms. Default 10 minutes. */\n ttlMs?: number\n /** Rendezvous URL override; falls back to the configured default. */\n rendezvousUrl?: string\n /** Mint a HOST-scoped offer (PR-C): the accepting side may register this\n * daemon as a driveable host (`devices add`), not just remote-control it.\n * Recorded on the resulting `OfferEntry`/`PairingRecord` server-side — this\n * is the authoritative record of what was granted, independent of the\n * `scope` param an accepting client's offer URL happens to carry. */\n scope?: \"host\"\n}\n\nexport interface PairingRegistryDeps {\n /** Lazily load (create on first use) the daemon identity. Kept lazy so a\n * daemon that never pairs never writes an identity file. */\n loadIdentity: () => Promise<DaemonIdentity>\n /** Path to `pairings.json`. Defaults to `~/.agentproto/pairings.json`. */\n pairingsPath?: string\n /** Configured rendezvous URL (from `config.pairing.rendezvous`). Three states:\n * - `undefined` (key absent) → `createOffer` falls back to the hosted default\n * (`HOSTED_RENDEZVOUS_URL`).\n * - a non-empty URL → that endpoint is the default for offers.\n * - `\"\"` (explicitly empty) → an explicit opt-out: no default applies, so an\n * offer without `--rendezvous` fails closed. Wire it through verbatim (do\n * not coerce `\"\"` to `undefined`) or the opt-out silently reverts to the\n * hosted default. */\n defaultRendezvousUrl?: string\n /** Dial a rendezvous WS and adapt it to a FrameSink. Injected by the CLI\n * (uses `ws` + `wrapWebSocket`). Rejects if the dial fails. The `signal`\n * aborts on registry shutdown. */\n dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>\n /** Serve the E2E-wrapped channel (the CLI builds `createTunnelServer` over\n * it with the same config `serve --connect` uses). */\n serve: (sink: E2eFrameSink, ctx: PairingChannelContext) => PairingChannelHandle\n /** Injectable clock (ms). Defaults to Date.now. */\n now?: () => number\n /** Diagnostic log sink. */\n log?: (line: string) => void\n /** Reconnect backoff floor / ceiling (ms). Defaults 1s / 30s. */\n reconnectMinMs?: number\n reconnectMaxMs?: number\n /** How long a parked daemon socket waits for a client hello before recycling\n * the connection. Kept just under the broker's park timeout. Default 110s. */\n handshakeTimeoutMs?: number\n /** How long a revoked pairing is still answered with `pairing_revoked` (see\n * \"Revocation\" above). Default 14 days; `0` disables tombstones, so a\n * revoked client just stops finding the daemon. */\n revokedGraceMs?: number\n}\n\nexport interface PairingRegistry {\n /** Mint a one-time offer + start dialing the rendezvous for it. */\n createOffer(input?: CreateOfferInput): Promise<CreatedOffer>\n /** All persisted pairings (copies). Loads `pairings.json` on first call. */\n list(): Promise<PairingRecord[]>\n /** Rename a pairing by fingerprint or name (cosmetic only — no effect on\n * auth or routing). Returns false when nothing matched; throws on an\n * empty `newName`. */\n rename(idOrName: string, newName: string): Promise<boolean>\n /** Drop a pairing by fingerprint or name; stops its rendezvous connections.\n * Returns false when nothing matched. */\n revoke(idOrName: string): Promise<boolean>\n /** Is a channel (offer or reconnect) currently served for this fingerprint? */\n isOnline(fingerprint: string): boolean\n /** Start standing reconnect connections for every persisted pairing. Call\n * after the gateway is up (so the injected `serve` can reach it). */\n startAutoconnect(): Promise<void>\n /** Tear down every offer + reconnect connection and served channel. */\n shutdown(): Promise<void>\n}\n\n/** What `mintLocalDevice` hands back. `bearer` is shown ONCE to the caller\n * (write it to the client's config); the host never logs or stores it. */\nexport interface LocalDeviceCredential {\n bearer: string\n fingerprint: string\n name: string\n createdAt: string\n}\n\n/** The registry `createPairingRegistry` returns: the daemon-facing\n * `PairingRegistry` plus the optional local-device credential. */\nexport interface PairingHostRegistry extends PairingRegistry {\n /** Mint a paired-device record for a loopback client and return its bearer.\n * Visible in `list()`, revocable with `revoke()`. Throws on an empty name. */\n mintLocalDevice(input: { name: string }): Promise<LocalDeviceCredential>\n /** Constant-time check of a device bearer. `null` for anything unknown,\n * malformed or revoked; a revoke is honoured on the very next call. */\n verifyDeviceBearer(bearer: string): Promise<{ fingerprint: string; name: string } | null>\n}\n\nconst DEFAULT_TTL_MS = 10 * 60_000\nconst DEFAULT_RECONNECT_MIN_MS = 1_000\nconst DEFAULT_RECONNECT_MAX_MS = 30_000\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 110_000\nconst DEFAULT_REVOKED_GRACE_MS = 14 * 86_400_000\n\nfunction defaultPairingsPath(): string {\n return join(homedir(), \".agentproto\", \"pairings.json\")\n}\n\n/**\n * Read-only snapshot of `pairings.json` for callers that just want the\n * persisted records — no `dial`/`serve`/`loadIdentity` deps, no rendezvous\n * connections started (e.g. `agentproto doctor`'s devices check). Mirrors\n * `ensureLoaded`'s parse + v1-legacy flagging; never throws — a missing or\n * malformed file just yields `[]`.\n */\nexport async function readPairingsSnapshot(path?: string): Promise<PairingRecord[]> {\n try {\n const raw = await readFile(path ?? defaultPairingsPath(), \"utf8\")\n const parsed: unknown = JSON.parse(raw)\n if (!isPairingsFile(parsed)) return []\n const fromV1 = parsed.v === LEGACY_PAIRINGS_VERSION\n return parsed.pairings.map(rec => (fromV1 ? { ...rec, legacy: true } : rec))\n } catch {\n return []\n }\n}\n\nfunction b64url(bytes: Buffer): string {\n return bytes.toString(\"base64\").replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n\n/** Constant-time string compare (HMAC-blind so length never leaks). Mirrors the\n * rendezvous/relay `timing-safe` pattern; inlined so runtime gains no dep. */\nfunction constantTimeEqual(a: string, b: string): boolean {\n const key = randomBytes(32)\n const da = createHmac(\"sha256\", key).update(a, \"utf8\").digest()\n const db = createHmac(\"sha256\", key).update(b, \"utf8\").digest()\n return timingSafeEqual(da, db)\n}\n\ninterface OfferEntry {\n /** Broker route derived from the offer secret (the map key). */\n route: string\n /** Auth token derived from the offer secret; the sealed hello must carry it. */\n auth: string\n exp: number // unix seconds\n spent: boolean\n rendezvousUrl: string\n /** This daemon's own recorded intent for the offer it minted (PR-C) — set\n * by `createOffer`'s caller, never by anything a client's hello claims\n * (the hello has no scope field). Written onto the resulting\n * `PairingRecord` on success. */\n scope?: \"host\"\n}\n\n/** Re-pair notice sent (E2E, inside a completed v1 channel) to a pair/v1\n * client. A v1 client drops tunnel-level `error` frames and only surfaces a\n * `hello` whose `version` it doesn't speak — as \"Tunnel daemon speaks\n * <version>; client speaks …\" — so the notice rides in that field too. */\nconst OUTDATED_HELLO_VERSION = `pair/v2 required — ${PAIRING_PROTOCOL_OUTDATED_MESSAGE}`\n\n/** One managed rendezvous connection loop (offer or a single epoch). */\ninterface ConnectionLoop {\n abort: AbortController\n /** Resolves when the loop has fully stopped. */\n done: Promise<void>\n}\n\nexport function createPairingRegistry(deps: PairingRegistryDeps): PairingHostRegistry {\n const pairingsPath = deps.pairingsPath ?? defaultPairingsPath()\n const now = deps.now ?? Date.now\n const log = deps.log ?? (() => {})\n const reconnectMinMs = deps.reconnectMinMs ?? DEFAULT_RECONNECT_MIN_MS\n const reconnectMaxMs = deps.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS\n const handshakeTimeoutMs = deps.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const revokedGraceMs = deps.revokedGraceMs ?? DEFAULT_REVOKED_GRACE_MS\n\n // Rate-limit dial-failure logging per loop key. A revoked/offline peer's\n // standing reconnect re-dials forever; without this a single dead pairing\n // buries daemon.log (it was ~85% of one 2.9MB log). First failure logs at\n // once, then at most one line per window carrying the suppressed count;\n // a successful dial resets the key. Backoff/timing is unchanged.\n const dialFailureGate = createReconnectLogGate({ now })\n\n /** fingerprint → record. Source of truth in memory; disk is the mirror. */\n const pairings = new Map<string, PairingRecord>()\n /** fingerprint → count of channels currently served (offer or reconnect).\n * Never persisted — a fresh boot starts with nothing online until a\n * reconnect loop pairs. */\n const onlineCounts = new Map<string, number>()\n /** fingerprint → tombstone of a revoked pairing (see \"Revocation\"). */\n const revoked = new Map<string, RevokedPairingRecord>()\n /** route → offer. */\n const offers = new Map<string, OfferEntry>()\n /** Loops keyed for lifecycle: `offer:<route>` and `pair:<fp>:<slot>`. Keys\n * reach the log, so they carry the (public) route, never a secret. */\n const loops = new Map<string, ConnectionLoop>()\n /** Live served channels, so shutdown/revoke can close them. */\n const channels = new Set<PairingChannelHandle>()\n\n /** fingerprint → local-device record (with its secret). */\n const localDevices = new Map<string, LocalDeviceRecord>()\n /** Identity (mtime/size/inode) of `pairings.json` as last read or written by\n * this registry; a mismatch means another process changed it. */\n let fileSig = \"\"\n\n let shuttingDown = false\n\n async function sigOfFile(): Promise<string | null> {\n try {\n const st = await stat(pairingsPath)\n return `${st.mtimeMs}:${st.size}:${st.ino}`\n } catch (err) {\n if (isEnoent(err)) return null\n throw err\n }\n }\n\n function replaceLocalDevices(records: unknown): void {\n localDevices.clear()\n if (!Array.isArray(records)) return\n for (const rec of records) if (isLocalDeviceRecord(rec)) localDevices.set(rec.fingerprint, rec)\n }\n\n /** Re-read `localDevices` if another process rewrote `pairings.json`. Only\n * the local-device map is replaced; pairings stay as this registry holds\n * them. A vanished file clears the map (fail closed); an unreadable one\n * keeps the previous view. */\n async function refreshLocalDevices(): Promise<void> {\n let sig: string | null\n try {\n sig = await sigOfFile()\n } catch {\n return\n }\n if (sig === null) {\n localDevices.clear()\n fileSig = \"\"\n return\n }\n if (sig === fileSig) return\n try {\n const parsed: unknown = JSON.parse(await readFile(pairingsPath, \"utf8\"))\n if (isPairingsFile(parsed)) {\n replaceLocalDevices(parsed.localDevices)\n fileSig = sig\n }\n } catch {\n /* mid-write or corrupt: keep the last good view */\n }\n }\n\n // Pairings are loaded lazily (ensureLoaded) on the first list/offer/autoconnect\n // so construction stays synchronous and touches no disk. One shared promise,\n // so a concurrent second caller waits for the load instead of seeing an\n // empty registry.\n let loadPromise: Promise<void> | null = null\n function ensureLoaded(): Promise<void> {\n loadPromise ??= doLoad()\n return loadPromise\n }\n async function doLoad(): Promise<void> {\n try {\n const sig = await sigOfFile()\n const raw = await readFile(pairingsPath, \"utf8\")\n const parsed: unknown = JSON.parse(raw)\n if (isPairingsFile(parsed)) {\n if (sig !== null) fileSig = sig\n replaceLocalDevices(parsed.localDevices)\n const fromV1 = parsed.v === LEGACY_PAIRINGS_VERSION\n for (const rec of parsed.pairings) {\n pairings.set(rec.fingerprint, fromV1 ? { ...rec, legacy: true } : rec)\n }\n for (const rec of parsed.revoked ?? []) {\n if (isRevokedRecord(rec) && tombstoneLive(rec)) revoked.set(rec.fingerprint, rec)\n }\n const legacy = Array.from(pairings.values()).filter(r => r.legacy)\n if (legacy.length > 0) {\n log(\n `[pairing] ${legacy.length} pairing(s) in ${pairingsPath} use the retired pair/v1 ` +\n `protocol and will not be served (${legacy.map(r => r.name).join(\", \")}). ` +\n \"Re-pair each client — run `agentproto pair offer` — then \" +\n \"`agentproto pair revoke <name>` the legacy entry.\",\n )\n }\n } else {\n log(`[pairing] ignoring ${pairingsPath}: unrecognised format`)\n }\n } catch (err) {\n if (!isEnoent(err)) {\n log(`[pairing] could not read ${pairingsPath}: ${errMsg(err)}`)\n }\n }\n }\n\n // Serialize persist() calls through one chain: two calls racing (e.g. a\n // reconnect's `lastSeen` update against a `rename`/`revoke` a user just\n // triggered) would otherwise share the same pid-keyed tmp filename AND —\n // worse — could finish out of start order, so whichever rename() landed\n // last would silently overwrite newer in-memory state with a stale\n // snapshot. Chaining makes each write wait its turn and read `pairings`/\n // `revoked` fresh at that turn, so writes land in call order and every\n // update is durably persisted.\n let persistChain: Promise<void> = Promise.resolve()\n function persist(): Promise<void> {\n const run = persistChain.then(() => doPersist())\n persistChain = run.catch(() => {})\n return run\n }\n /** Apply a local-device change and write it, serialized with every other\n * write: refresh from disk first so a change another process made is not\n * clobbered by this registry's stale view. */\n function mutateLocal(apply: () => void): Promise<void> {\n const run = persistChain.then(async () => {\n await refreshLocalDevices()\n apply()\n await doPersist(false)\n })\n persistChain = run.catch(() => {})\n return run\n }\n async function doPersist(refresh = true): Promise<void> {\n if (refresh) await refreshLocalDevices()\n for (const [fp, rec] of revoked) if (!tombstoneLive(rec)) revoked.delete(fp)\n const file: PairingsFile = {\n v: PAIRINGS_VERSION,\n pairings: Array.from(pairings.values()),\n ...(revoked.size > 0 ? { revoked: Array.from(revoked.values()) } : {}),\n ...(localDevices.size > 0 ? { localDevices: Array.from(localDevices.values()) } : {}),\n }\n const dir = dirname(pairingsPath)\n await mkdir(dir, { recursive: true })\n const tmp = join(dir, `.${basename(pairingsPath)}.tmp-${process.pid}`)\n await writeFile(tmp, JSON.stringify(file, null, 2) + \"\\n\", { encoding: \"utf8\", mode: 0o600 })\n await chmod(tmp, 0o600).catch(() => {})\n await rename(tmp, pairingsPath)\n fileSig = (await sigOfFile()) ?? \"\"\n }\n\n // ── connection loop ────────────────────────────────────────────\n\n interface LoopSpec {\n key: string\n rendezvousUrl: string\n /** Route (dialled) + auth (verified) tokens for this iteration —\n * recomputed for epoch loops; the derivation is async. */\n tokens: () => RouteAuthTokens | Promise<RouteAuthTokens>\n /** Verify the auth token presented in the sealed hello against this\n * iteration's `tokens()`. Must be constant-time. */\n verify: (presented: string, expected: RouteAuthTokens) => boolean\n /** Whether to stop after the first successful serve (offers) vs. loop\n * forever re-parking (reconnect). */\n singleUse: boolean\n /** Should the loop keep running? (e.g. offer not yet expired/spent, or the\n * pairing still exists). Checked at the top of each iteration. */\n shouldContinue: () => boolean\n /** Set for a revoked pairing's tombstone: after the handshake, send this\n * frame and close instead of serving (`onPaired` is not called). */\n reject?: ErrorFrame\n /** Build the serve context + persist side-effects on a successful pair. */\n onPaired: (\n session: import(\"@agentproto/secrets/pairing\").PairingSession,\n hello: import(\"@agentproto/secrets/pairing\").PairingHello,\n ) => Promise<PairingChannelContext>\n }\n\n function startLoop(spec: LoopSpec): void {\n if (loops.has(spec.key)) return\n const abort = new AbortController()\n const done = runLoop(spec, abort.signal).catch(err => {\n log(`[pairing] loop ${spec.key} ended: ${errMsg(err)}`)\n })\n loops.set(spec.key, { abort, done })\n }\n\n async function stopLoop(key: string, reason?: typeof REVOKED): Promise<void> {\n const loop = loops.get(key)\n if (!loop) return\n loops.delete(key)\n loop.abort.abort(reason)\n await loop.done.catch(() => {})\n }\n\n async function runLoop(spec: LoopSpec, signal: AbortSignal): Promise<void> {\n let backoff = reconnectMinMs\n while (!signal.aborted && spec.shouldContinue()) {\n const expected = await spec.tokens()\n let sink: FrameSink\n try {\n sink = await deps.dial(dialUrl(spec.rendezvousUrl, expected.route), signal)\n } catch (err) {\n if (signal.aborted) break\n const line = dialFailureGate.onFailure(\n spec.key,\n `[pairing] dial ${spec.key} failed: ${errMsg(err)}`,\n )\n if (line) log(line)\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n // A socket the broker refused on arrival (e.g. `token_in_use` while the\n // previous splice on this route is still tearing down) can come back\n // already closed: its close event fired before we could subscribe, so the\n // handshake would sit out its whole timeout. Treat it as a failed dial.\n if (!sink.isOpen) {\n if (signal.aborted) break\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n // Dial succeeded — clear any accumulated suppression for this key so the\n // next outage logs its first failure promptly.\n dialFailureGate.onSuccess(spec.key)\n\n // Make the in-flight handshake abortable: closing the sink on shutdown\n // makes `daemonHandshakeOverSink` reject promptly (transport closed),\n // instead of blocking on its own timeout for up to handshakeTimeoutMs.\n let serving = false\n const onAbort = (): void => {\n // A revoke of a live channel is handled by waitClosed, which tells the\n // client before closing — closing the raw socket here would drop that.\n if (serving && signal.reason === REVOKED) return\n try {\n sink.close(\"registry shutdown\")\n } catch {\n /* already closing */\n }\n }\n signal.addEventListener(\"abort\", onAbort, { once: true })\n\n try {\n let capturedSession: import(\"@agentproto/secrets/pairing\").PairingSession | null = null\n let capturedHello: import(\"@agentproto/secrets/pairing\").PairingHello | null = null\n let legacyPeer = false\n let wrapped: E2eFrameSink\n try {\n const identity = await deps.loadIdentity()\n wrapped = await daemonHandshakeOverSink(\n sink,\n async helloBytes => {\n let hello: import(\"@agentproto/secrets/pairing\").PairingHello\n try {\n hello = decodePairingHello(helloBytes)\n } catch (err) {\n if (!(err instanceof PairingError) || err.code !== \"pairing_protocol_outdated\") throw err\n // A pair/v1 client: complete ITS handshake only to tell it to\n // re-pair. No auth is checked and nothing is served.\n legacyPeer = true\n return respondToLegacyHandshake(helloBytes, identity)\n }\n const result = await respondToHandshake(hello, {\n identity,\n verifyAuthToken: presented => spec.verify(presented, expected),\n })\n capturedSession = result.session\n capturedHello = hello\n return { reply: encodePairingMessage(result.reply), keys: result.session }\n },\n { timeoutMs: handshakeTimeoutMs },\n )\n } catch (err) {\n if (signal.aborted) break\n // A park timeout, a rejected offer, or a tampered hello. Fail closed\n // and re-dial after a short backoff (park timeouts are common + benign).\n void err\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n\n if (legacyPeer) {\n sendOutdatedNotice(wrapped)\n log(`[pairing] told a pair/v1 client on ${spec.key} to re-pair`)\n // Not a success: keep backing off so a v1 client (or anyone replaying\n // a v1 hello) can't spin this loop.\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n\n backoff = reconnectMinMs // success resets\n\n if (!capturedSession || !capturedHello) {\n wrapped.close(\"internal: missing session\")\n continue\n }\n\n if (spec.reject) {\n // Tombstone: the client proved possession of a revoked pairing's\n // epoch AUTH token and verified our signature — tell it, inside the\n // AEAD channel, that it is unpaired. Never served. `close` flushes\n // the frame first.\n wrapped.send(spec.reject)\n wrapped.close(spec.reject.code)\n log(`[pairing] refused revoked pairing via ${spec.key}`)\n continue\n }\n\n let ctx: PairingChannelContext\n try {\n ctx = await spec.onPaired(capturedSession, capturedHello)\n } catch (err) {\n log(`[pairing] persist for ${spec.key} failed: ${errMsg(err)}`)\n wrapped.close(\"persist failed\")\n continue\n }\n\n serving = true\n const handle = deps.serve(wrapped, ctx)\n channels.add(handle)\n onlineCounts.set(ctx.fingerprint, (onlineCounts.get(ctx.fingerprint) ?? 0) + 1)\n log(`[pairing] channel up (${ctx.mode}) for ${ctx.fingerprint} via ${spec.key}`)\n\n await waitClosed(wrapped, signal)\n channels.delete(handle)\n await handle.close().catch(() => {})\n const remaining = (onlineCounts.get(ctx.fingerprint) ?? 1) - 1\n if (remaining > 0) onlineCounts.set(ctx.fingerprint, remaining)\n else onlineCounts.delete(ctx.fingerprint)\n log(`[pairing] channel closed (${ctx.mode}) for ${ctx.fingerprint}`)\n\n if (spec.singleUse) break\n } finally {\n signal.removeEventListener(\"abort\", onAbort)\n }\n }\n }\n\n // ── offer handling ─────────────────────────────────────────────\n\n async function createOffer(input: CreateOfferInput = {}): Promise<CreatedOffer> {\n await ensureLoaded()\n // Precedence (PLAN deliverable 2): `--rendezvous` flag → `pairing.rendezvous`\n // in config → hosted default. A configured empty string is an explicit\n // opt-out (deliverable 4b): no default applies, so an offer without an\n // explicit `--rendezvous` fails closed — the reachable form of the branch\n // that used to fire whenever nothing was configured.\n let rendezvousUrl: string\n let rendezvousIsHostedDefault = false\n if (input.rendezvousUrl) {\n rendezvousUrl = input.rendezvousUrl\n } else if (deps.defaultRendezvousUrl !== undefined) {\n if (deps.defaultRendezvousUrl === \"\") {\n throw new PairingError(\n \"malformed_offer\",\n 'rendezvous disabled — pairing.rendezvous is set to \"\" (explicit opt-out); ' +\n \"pass --rendezvous to route this offer through a specific broker\",\n )\n }\n rendezvousUrl = deps.defaultRendezvousUrl\n } else {\n rendezvousUrl = HOSTED_RENDEZVOUS_URL\n rendezvousIsHostedDefault = true\n }\n const identity = await deps.loadIdentity()\n const secret = b64url(randomBytes(16))\n const { route, auth } = await deriveOfferTokens(secret)\n const ttlMs = input.ttlMs ?? DEFAULT_TTL_MS\n const exp = Math.floor((now() + ttlMs) / 1000)\n const scope = input.scope\n offers.set(route, { route, auth, exp, spent: false, rendezvousUrl, ...(scope ? { scope } : {}) })\n\n const fingerprint = await identityFingerprint(identity.x25519.pub)\n const url = encodeOfferUrl({\n v: OFFER_VERSION,\n rendezvousUrl,\n fingerprint,\n daemonX25519Pub: identity.x25519.pub,\n daemonEd25519Pub: identity.ed25519.pub,\n secret,\n exp,\n ...(scope ? { scope } : {}),\n })\n\n startOfferLoop({ route, auth }, rendezvousUrl)\n log(\n `[pairing] offer minted (exp ${new Date(exp * 1000).toISOString()}) via ${rendezvousUrl}` +\n (rendezvousIsHostedDefault ? \" (hosted default)\" : \"\") +\n (scope === \"host\" ? \" [HOST-SCOPED]\" : \"\"),\n )\n return { secret, exp, url, fingerprint, rendezvousUrl, rendezvousIsHostedDefault, ...(scope ? { scope } : {}) }\n }\n\n function offerValid(route: string): boolean {\n const entry = offers.get(route)\n return !!entry && !entry.spent && entry.exp * 1000 > now()\n }\n\n function startOfferLoop(tokens: RouteAuthTokens, rendezvousUrl: string): void {\n const { route } = tokens\n startLoop({\n key: `offer:${route}`,\n rendezvousUrl,\n tokens: () => tokens,\n shouldContinue: () => offerValid(route),\n singleUse: true,\n verify: presented => {\n const entry = offers.get(route)\n if (!entry || entry.spent || entry.exp * 1000 <= now()) return false\n // The AUTH token, never the route: the broker knows the route.\n if (!constantTimeEqual(presented, entry.auth)) return false\n entry.spent = true // SPEND — single use, and only on a correct auth\n return true\n },\n onPaired: async (session, hello) => {\n const pairRoot = await derivePairRoot(session)\n const fingerprint = session.peerFingerprint\n const nowIso = new Date(now()).toISOString()\n // The offer's OWN recorded scope — never anything the client's hello\n // claims (it has no scope field at all). `entry` is this offer's\n // OfferEntry, looked up by route, which `verify` already confirmed\n // above (it only returns true for a live, unspent, matching entry).\n const entry = offers.get(route)\n const record: PairingRecord = {\n clientPub: hello.ePub,\n name: session.clientName ?? fingerprint,\n fingerprint,\n createdAt: pairings.get(fingerprint)?.createdAt ?? nowIso,\n lastSeen: nowIso,\n pairRoot,\n rendezvousUrl,\n ...(entry?.scope ? { scope: entry.scope } : {}),\n }\n pairings.set(fingerprint, record)\n revoked.delete(fingerprint)\n await persist()\n // Start standing reconnect connections so the client can come back.\n startReconnectLoops(record)\n return { mode: \"offer\", fingerprint, name: record.name, ...(record.scope ? { scope: record.scope } : {}) }\n },\n })\n }\n\n // ── reconnect handling ─────────────────────────────────────────\n\n function startReconnectLoops(record: PairingRecord): void {\n // Two standing connections — current + previous epoch — so a client whose\n // clock sits on either side of the UTC-day boundary can still splice.\n for (const slot of [\"cur\", \"prev\"] as const) {\n const epochOf = () => (slot === \"cur\" ? currentEpoch(now()) : currentEpoch(now()) - 1)\n startLoop({\n key: `pair:${record.fingerprint}:${slot}`,\n rendezvousUrl: record.rendezvousUrl,\n tokens: () => deriveEpochTokens(record.pairRoot, epochOf()),\n shouldContinue: () => pairings.has(record.fingerprint),\n singleUse: false,\n // A legacy (v1) pairing is parked on only so its client can be told to\n // re-pair; it never authenticates, whatever it presents.\n verify: (presented, expected) =>\n !record.legacy && constantTimeEqual(presented, expected.auth),\n onPaired: async (session, _hello) => {\n const existing = pairings.get(record.fingerprint)\n if (existing) {\n existing.lastSeen = new Date(now()).toISOString()\n await persist().catch(err => log(`[pairing] lastSeen persist failed: ${errMsg(err)}`))\n }\n // Silence unused-session lint while keeping the signature uniform.\n void session\n return {\n mode: \"reconnect\",\n fingerprint: record.fingerprint,\n name: existing?.name ?? record.name,\n ...(record.scope ? { scope: record.scope } : {}),\n }\n },\n })\n }\n }\n\n function startTombstoneLoops(rec: RevokedPairingRecord): void {\n for (const slot of [\"cur\", \"prev\"] as const) {\n const epochOf = () => (slot === \"cur\" ? currentEpoch(now()) : currentEpoch(now()) - 1)\n const tokensFor = () => rec.routes.find(r => r.epoch === epochOf())\n startLoop({\n key: `revoked:${rec.fingerprint}:${slot}`,\n rendezvousUrl: rec.rendezvousUrl,\n tokens: () => tokensFor() ?? { route: \"\", auth: \"\" },\n shouldContinue: () =>\n revoked.get(rec.fingerprint) === rec && tombstoneLive(rec) && tokensFor() !== undefined,\n singleUse: false,\n // The AUTH token, never the route: the broker knows every route, and a\n // tombstone must not become a way for it to probe revocation state.\n verify: (presented, expected) => expected.auth !== \"\" && constantTimeEqual(presented, expected.auth),\n reject: revokedFrame(),\n onPaired: async () => {\n throw new Error(\"unreachable: a tombstone never serves\")\n },\n })\n }\n }\n\n function tombstoneLive(rec: RevokedPairingRecord): boolean {\n return Date.parse(rec.expiresAt) > now()\n }\n\n async function startAutoconnect(): Promise<void> {\n await ensureLoaded()\n for (const record of pairings.values()) {\n startReconnectLoops(record)\n }\n for (const rec of revoked.values()) {\n startTombstoneLoops(rec)\n }\n if (pairings.size > 0) {\n log(`[pairing] autoconnect: standing connections for ${pairings.size} pairing(s)`)\n }\n }\n\n // ── revocation ─────────────────────────────────────────────────\n\n async function revoke(idOrName: string): Promise<boolean> {\n await ensureLoaded()\n let target: PairingRecord | undefined = pairings.get(idOrName)\n if (!target) {\n for (const rec of pairings.values()) {\n if (rec.name === idOrName) {\n target = rec\n break\n }\n }\n }\n if (!target) {\n await refreshLocalDevices()\n const device = findLocalDevice(idOrName)\n if (!device) return false\n // No tombstone, no loops: a local device never rendezvoused. The map is\n // updated before this resolves, so the next verify already rejects it.\n await mutateLocal(() => void localDevices.delete(device.fingerprint))\n log(`[pairing] revoked local device ${device.fingerprint} (${device.name})`)\n return true\n }\n pairings.delete(target.fingerprint)\n let tombstone: RevokedPairingRecord | null = null\n // No tombstone for a legacy (v1) pairing: its client can only be told to\n // re-pair (see \"Legacy\"), which a v1 client can't act on as \"revoked\".\n if (revokedGraceMs > 0 && !target.legacy) {\n const at = now()\n const expiresAt = at + revokedGraceMs\n const routes: RevokedPairingRecord[\"routes\"] = []\n for (let e = currentEpoch(at) - 1; e <= currentEpoch(expiresAt); e++) {\n routes.push({ epoch: e, ...(await deriveEpochTokens(target.pairRoot, e)) })\n }\n tombstone = {\n fingerprint: target.fingerprint,\n name: target.name,\n rendezvousUrl: target.rendezvousUrl,\n revokedAt: new Date(at).toISOString(),\n expiresAt: new Date(expiresAt).toISOString(),\n routes,\n }\n revoked.set(target.fingerprint, tombstone)\n }\n await persist()\n // Stop serving/parking on this pairing's tokens. A live channel is told\n // `pairing_revoked` before it closes (see waitClosed).\n await stopLoop(`pair:${target.fingerprint}:cur`, REVOKED)\n await stopLoop(`pair:${target.fingerprint}:prev`, REVOKED)\n if (tombstone) startTombstoneLoops(tombstone)\n log(`[pairing] revoked ${target.fingerprint} (${target.name})`)\n return true\n }\n\n async function list(): Promise<PairingRecord[]> {\n await ensureLoaded()\n await refreshLocalDevices()\n return [\n ...Array.from(pairings.values()).map(r => ({ ...r })),\n ...Array.from(localDevices.values()).map(localDeviceView),\n ]\n }\n\n function findLocalDevice(idOrName: string): LocalDeviceRecord | undefined {\n const byFp = localDevices.get(idOrName)\n if (byFp) return byFp\n for (const rec of localDevices.values()) if (rec.name === idOrName) return rec\n return undefined\n }\n\n function localDeviceView(rec: LocalDeviceRecord): PairingRecord {\n return {\n clientPub: \"\",\n name: rec.name,\n fingerprint: rec.fingerprint,\n createdAt: rec.createdAt,\n lastSeen: rec.createdAt,\n pairRoot: \"\",\n rendezvousUrl: \"\",\n local: true,\n }\n }\n\n async function mintLocalDevice(input: { name: string }): Promise<LocalDeviceCredential> {\n const name = input.name.trim()\n if (!name) throw new Error(\"device name must not be empty\")\n await ensureLoaded()\n const rec = newLocalDevice(name, new Date(now()).toISOString())\n await mutateLocal(() => void localDevices.set(rec.fingerprint, rec))\n // The bearer is deliberately absent from this line.\n log(`[pairing] minted local device ${rec.fingerprint} (${rec.name})`)\n return {\n bearer: deriveLocalBearer(rec),\n fingerprint: rec.fingerprint,\n name: rec.name,\n createdAt: rec.createdAt,\n }\n }\n\n async function verifyDeviceBearer(\n bearer: string,\n ): Promise<{ fingerprint: string; name: string } | null> {\n await ensureLoaded()\n await refreshLocalDevices()\n const parsed = parseLocalBearer(bearer)\n const rec = parsed ? localDevices.get(parsed.fingerprint) : undefined\n return verifyLocalBearer(rec, parsed) && rec ? { fingerprint: rec.fingerprint, name: rec.name } : null\n }\n\n // Named `renamePairing`, not `rename` — this closure also calls the\n // `node:fs/promises` `rename` (in `persist()`'s atomic write) and a same-\n // named local function here would shadow that import for the rest of the\n // closure, silently breaking every persist. Exposed on the returned\n // registry as `rename`; only the internal binding avoids the collision.\n async function renamePairing(idOrName: string, newName: string): Promise<boolean> {\n const trimmed = newName.trim()\n if (!trimmed) throw new Error(\"device name must not be empty\")\n await ensureLoaded()\n let target: PairingRecord | undefined = pairings.get(idOrName)\n if (!target) {\n for (const rec of pairings.values()) {\n if (rec.name === idOrName) {\n target = rec\n break\n }\n }\n }\n if (!target) {\n await refreshLocalDevices()\n const device = findLocalDevice(idOrName)\n if (!device) return false\n await mutateLocal(() => {\n const current = localDevices.get(device.fingerprint)\n if (current) current.name = trimmed\n })\n log(`[pairing] renamed ${device.fingerprint} to \"${trimmed}\"`)\n return true\n }\n target.name = trimmed\n await persist()\n log(`[pairing] renamed ${target.fingerprint} to \"${trimmed}\"`)\n return true\n }\n\n function isOnline(fingerprint: string): boolean {\n return (onlineCounts.get(fingerprint) ?? 0) > 0\n }\n\n async function shutdown(): Promise<void> {\n if (shuttingDown) return\n shuttingDown = true\n for (const key of Array.from(loops.keys())) {\n await stopLoop(key)\n }\n for (const handle of Array.from(channels)) {\n await handle.close().catch(() => {})\n }\n channels.clear()\n }\n\n return {\n createOffer,\n list,\n rename: renamePairing,\n revoke,\n isOnline,\n startAutoconnect,\n shutdown,\n mintLocalDevice,\n verifyDeviceBearer,\n }\n}\n\n// ── helpers ────────────────────────────────────────────────────\n\nfunction dialUrl(rendezvousUrl: string, token: string): string {\n const sep = rendezvousUrl.includes(\"?\") ? \"&\" : \"?\"\n return `${rendezvousUrl}${sep}side=daemon&t=${encodeURIComponent(token)}`\n}\n\n/** Tell a pair/v1 peer to re-pair, over the notice-only channel, then close. */\nfunction sendOutdatedNotice(sink: E2eFrameSink): void {\n sink.send({ t: \"error\", code: \"pairing_protocol_outdated\", message: PAIRING_PROTOCOL_OUTDATED_MESSAGE })\n sink.send({\n t: \"hello\",\n // Deliberately not TUNNEL_VERSION: a v1 client reports an unknown version\n // verbatim, which is how the notice reaches its user (see\n // OUTDATED_HELLO_VERSION).\n version: OUTDATED_HELLO_VERSION as \"agentproto/tunnel/v1\",\n capabilities: { pty: false },\n label: \"pairing_protocol_outdated\",\n })\n // close() flushes already-queued frames before closing the transport.\n sink.close(\"pairing_protocol_outdated\")\n}\n\n/** Abort reason for a loop stopped by `revoke` — its live channel, if any, is\n * told `pairing_revoked` before it closes. */\nconst REVOKED = Symbol(\"pairing revoked\")\n\nfunction waitClosed(sink: E2eFrameSink, signal: AbortSignal): Promise<void> {\n return new Promise(resolve => {\n if (!sink.isOpen) {\n resolve()\n return\n }\n let settled = false\n const finish = (): void => {\n if (settled) return\n settled = true\n resolve()\n }\n sink.onClose(() => finish())\n signal.addEventListener(\"abort\", () => {\n if (signal.reason === REVOKED) {\n sink.send(revokedFrame())\n sink.close(PAIRING_REVOKED_CODE)\n } else {\n sink.close(\"registry shutdown\")\n }\n finish()\n })\n })\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise(resolve => {\n if (signal.aborted) return resolve()\n const timer = setTimeout(resolve, ms)\n if (typeof timer.unref === \"function\") timer.unref()\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer)\n resolve()\n })\n })\n}\n\nfunction isPairingsFile(v: unknown): v is PairingsFile {\n if (typeof v !== \"object\" || v === null) return false\n const rec = v as Record<string, unknown>\n if (rec[\"v\"] !== PAIRINGS_VERSION && rec[\"v\"] !== LEGACY_PAIRINGS_VERSION) return false\n if (!Array.isArray(rec[\"pairings\"])) return false\n return rec[\"pairings\"].every(isPairingRecord)\n}\n\nfunction isPairingRecord(v: unknown): v is PairingRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"clientPub\"] === \"string\" &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"fingerprint\"] === \"string\" &&\n typeof r[\"createdAt\"] === \"string\" &&\n typeof r[\"lastSeen\"] === \"string\" &&\n typeof r[\"pairRoot\"] === \"string\" &&\n typeof r[\"rendezvousUrl\"] === \"string\"\n )\n}\n\nfunction isRevokedRecord(v: unknown): v is RevokedPairingRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"fingerprint\"] === \"string\" &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"rendezvousUrl\"] === \"string\" &&\n typeof r[\"revokedAt\"] === \"string\" &&\n typeof r[\"expiresAt\"] === \"string\" &&\n Array.isArray(r[\"routes\"]) &&\n r[\"routes\"].every(x => {\n if (typeof x !== \"object\" || x === null) return false\n const t = x as Record<string, unknown>\n return typeof t[\"epoch\"] === \"number\" && typeof t[\"route\"] === \"string\" && typeof t[\"auth\"] === \"string\"\n })\n )\n}\n\nfunction isEnoent(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code?: unknown }).code === \"ENOENT\"\n )\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","import type { Agent } from \"node:http\"\nimport WebSocket from \"ws\"\nimport { wrapWebSocket, type FrameSink } from \"@agentproto/acp/tunnel\"\n\nexport interface DialRendezvousOptions {\n /** A `ws`-compatible agent (e.g. an HTTPS proxy agent). `ws` ignores\n * `HTTPS_PROXY` on its own, so a host behind a proxy passes one here. */\n agent?: Agent\n}\n\n/**\n * Dial a rendezvous broker outbound and adapt the socket to a `FrameSink`.\n * The default `PairingRegistryDeps.dial`. Rejects if the dial fails; honours\n * `signal` so a registry shutdown tears down an in-flight dial promptly.\n */\nexport async function dialRendezvous(\n url: string,\n signal: AbortSignal,\n opts: DialRendezvousOptions = {},\n): Promise<FrameSink> {\n const ws = new WebSocket(url, opts.agent ? { agent: opts.agent } : undefined)\n await new Promise<void>((resolve, reject) => {\n const cleanup = (): void => {\n ws.off(\"open\", onOpen)\n ws.off(\"error\", onError)\n signal.removeEventListener(\"abort\", onAbort)\n }\n const onOpen = (): void => {\n cleanup()\n resolve()\n }\n const onError = (err: Error): void => {\n cleanup()\n reject(err)\n }\n const onAbort = (): void => {\n cleanup()\n try {\n ws.close()\n } catch {\n /* ignore */\n }\n reject(new Error(\"dial aborted\"))\n }\n if (signal.aborted) {\n onAbort()\n return\n }\n ws.once(\"open\", onOpen)\n ws.once(\"error\", onError)\n signal.addEventListener(\"abort\", onAbort)\n })\n return wrapWebSocket(ws as unknown as Parameters<typeof wrapWebSocket>[0])\n}\n","import {\n createTunnelServer,\n DEFAULT_HTTP_FORWARD_TIMEOUT_MS,\n type E2eFrameSink,\n type TunnelFrame,\n} from \"@agentproto/acp/tunnel\"\nimport type { PairingChannelContext, PairingChannelHandle } from \"./pairing-registry.js\"\n\nexport interface ServeLoopbackHttpOptions {\n /** The local HTTP app the paired channel is forwarded to. Must be a\n * loopback http(s) URL (127.0.0.0/8, ::1 or localhost): a paired peer must\n * never be able to steer the host at another machine. */\n target: URL | string\n /** Headers set on EVERY forwarded request, overriding any same-named header\n * the peer sent (e.g. the host app's own bearer). */\n injectHeaders?: Readonly<Record<string, string>>\n /** Pathnames a peer may request. `\"/mcp\"` matches exactly, `\"/api/*\"`\n * matches `/api` and everything below it, a RegExp is tested against the\n * pathname. Omitted: any path. `[]`: nothing. A denied request gets a 403\n * `path_not_allowed` response and never reaches the target. */\n allowPaths?: readonly (string | RegExp)[]\n /** Label in the tunnel hello. Default \"loopback-http\". */\n label?: string\n httpForwardTimeoutMs?: number\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"])\n\nfunction isLoopback(url: URL): boolean {\n return LOOPBACK_HOSTS.has(url.hostname) || /^127\\.\\d+\\.\\d+\\.\\d+$/.test(url.hostname)\n}\n\n/** Pathname of a frame path, or null when it must be refused outright (the\n * tunnel server already rejects `..` and non-rooted paths; encoded dot/slash\n * forms are refused here because the target may decode them). */\nfunction safePathname(path: string): string | null {\n if (!path.startsWith(\"/\") || path.startsWith(\"//\") || path.includes(\"\\\\\")) return null\n if (/%2e|%2f|%5c/i.test(path)) return null\n const end = path.search(/[?#]/)\n return end === -1 ? path : path.slice(0, end)\n}\n\nfunction pathAllowed(pathname: string, allow: readonly (string | RegExp)[]): boolean {\n return allow.some(rule => {\n if (rule instanceof RegExp) {\n rule.lastIndex = 0\n return rule.test(pathname)\n }\n if (rule.endsWith(\"/*\")) {\n const base = rule.slice(0, -2)\n return pathname === base || pathname.startsWith(`${base}/`)\n }\n return pathname === rule\n })\n}\n\n/**\n * Build a `PairingRegistryDeps.serve` that forwards the paired, E2E-wrapped\n * channel to a local HTTP app: `http_request` frames become fetches against\n * `target` (with `injectHeaders` applied), responses stream back. Spawn, PTY\n * and WebSocket frames are not served.\n */\nexport function serveLoopbackHttp(\n opts: ServeLoopbackHttpOptions,\n): (sink: E2eFrameSink, ctx: PairingChannelContext) => PairingChannelHandle {\n const target = typeof opts.target === \"string\" ? new URL(opts.target) : opts.target\n if (target.protocol !== \"http:\" && target.protocol !== \"https:\") {\n throw new Error(`serveLoopbackHttp: target must be http(s), got ${target.protocol}`)\n }\n if (!isLoopback(target)) {\n throw new Error(`serveLoopbackHttp: target must be a loopback address, got ${target.hostname}`)\n }\n const httpUpstream = target.origin + target.pathname.replace(/\\/$/, \"\")\n const allow = opts.allowPaths\n\n return sink => {\n const guarded = allow ? guardSink(sink, allow) : sink\n const server = createTunnelServer({\n sink: guarded,\n label: opts.label ?? \"loopback-http\",\n pty: false,\n authorize: () => null,\n httpUpstream,\n ...(opts.injectHeaders && Object.keys(opts.injectHeaders).length > 0\n ? { httpInjectHeaders: opts.injectHeaders }\n : {}),\n httpForwardTimeoutMs: opts.httpForwardTimeoutMs ?? DEFAULT_HTTP_FORWARD_TIMEOUT_MS,\n httpStreamIdleTimeoutMs: 120_000,\n })\n return { close: () => server.close() }\n }\n}\n\n/** A view of `sink` whose inbound `http_request` frames outside `allow` are\n * answered 403 here instead of being delivered to the tunnel server. */\nfunction guardSink(sink: E2eFrameSink, allow: readonly (string | RegExp)[]): E2eFrameSink {\n const denied = new Set<string>()\n const deny = (reqId: string, path: string): void => {\n denied.add(reqId)\n sink.send({\n t: \"http_response\",\n reqId,\n status: 403,\n error: { code: \"path_not_allowed\", message: `path '${path}' is not allowed on this host` },\n })\n }\n return {\n send: frame => sink.send(frame),\n close: reason => sink.close(reason),\n onClose: handler => sink.onClose(handler),\n get isOpen() {\n return sink.isOpen\n },\n get sentCount() {\n return sink.sentCount\n },\n get recvCount() {\n return sink.recvCount\n },\n onFrame: handler =>\n sink.onFrame((frame: TunnelFrame) => {\n if (frame.t === \"http_request\") {\n const pathname = safePathname(frame.path)\n if (pathname === null || !pathAllowed(pathname, allow)) {\n deny(frame.reqId, pathname ?? \"(invalid)\")\n return\n }\n } else if (frame.t === \"http_request_chunk\" && denied.has(frame.reqId)) {\n if (frame.end) denied.delete(frame.reqId)\n return\n }\n handler(frame)\n }),\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/reconnect-log-gate.ts","../src/local-device.ts","../src/pairing-registry.ts","../src/dial.ts","../src/loopback-http.ts"],"names":["randomBytes","createHmac","timingSafeEqual"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Rate-limited failure logging for reconnect loops.\n *\n * A standing reconnect loop (the outbound tunnel, a pairing rendezvous) whose\n * peer is permanently gone re-dials on backoff forever. Logging every single\n * failure buries the daemon log — a single dead pairing produced ~85% of one\n * 2.9MB never-rotated `~/.agentproto/daemon.log`, and the tunnel reconnect\n * spam another chunk. This gate logs the FIRST failure per key immediately,\n * then at most one line per `windowMs`, and when it does emit again it carries\n * the count of failures it swallowed in between. A successful connect resets\n * the key so the next outage logs promptly again.\n *\n * Deliberately dependency-free: a per-key `{ lastLoggedAt, suppressed }` map.\n * `now` is injectable so tests can advance the clock without real timers. This\n * only changes logging cadence — reconnect timing/backoff is untouched.\n */\nexport interface ReconnectLogGate {\n /**\n * Record a failure for `key`. Returns the line to log now — the given\n * `message`, with a `(N failures suppressed)` suffix when repeats were\n * swallowed since the last emit — or `null` to stay quiet this time.\n */\n onFailure(key: string, message: string): string | null\n /** Reset suppression for `key` after a successful connect. */\n onSuccess(key: string): void\n}\n\nexport function createReconnectLogGate(\n opts: { windowMs?: number; now?: () => number } = {},\n): ReconnectLogGate {\n const windowMs = opts.windowMs ?? 60_000\n const now = opts.now ?? Date.now\n const state = new Map<string, { lastLoggedAt: number; suppressed: number }>()\n return {\n onFailure(key, message) {\n const ts = now()\n const prev = state.get(key)\n if (prev && ts - prev.lastLoggedAt < windowMs) {\n prev.suppressed += 1\n return null\n }\n const suffix =\n prev && prev.suppressed > 0\n ? ` (${prev.suppressed} failure${prev.suppressed === 1 ? \"\" : \"s\"} suppressed)`\n : \"\"\n state.set(key, { lastLoggedAt: ts, suppressed: 0 })\n return `${message}${suffix}`\n },\n onSuccess(key) {\n state.delete(key)\n },\n }\n}\n","/**\n * Local-device bearer primitives (see \"Local devices\" in pairing-registry.ts).\n * Pure functions; the registry owns storage and file freshness.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\"\n\nexport const LOCAL_BEARER_PREFIX = \"apd1\"\nconst MAC_CONTEXT = \"agentproto/local-device/v1|\"\n\n/** One local device as persisted in `pairings.json` under `localDevices`. */\nexport interface LocalDeviceRecord {\n /** 32 lowercase hex chars: the public handle (also the bearer's lookup key). */\n fingerprint: string\n name: string\n /** ISO-8601. */\n createdAt: string\n /** Per-device secret (base64url, 32 bytes). Never leaves the host. */\n secret: string\n}\n\nexport function newLocalDevice(name: string, createdAt: string): LocalDeviceRecord {\n return {\n fingerprint: randomBytes(16).toString(\"hex\"),\n name,\n createdAt,\n secret: randomBytes(32).toString(\"base64url\"),\n }\n}\n\nfunction macOf(rec: Pick<LocalDeviceRecord, \"fingerprint\" | \"secret\">): Buffer {\n return createHmac(\"sha256\", Buffer.from(rec.secret, \"base64url\"))\n .update(MAC_CONTEXT + rec.fingerprint, \"utf8\")\n .digest()\n}\n\n/** `apd1.<fingerprint>.<mac>`: opaque to the client, re-derivable by the host. */\nexport function deriveLocalBearer(rec: LocalDeviceRecord): string {\n return `${LOCAL_BEARER_PREFIX}.${rec.fingerprint}.${macOf(rec).toString(\"base64url\")}`\n}\n\nexport interface ParsedBearer {\n fingerprint: string\n /** Raw base64url as presented (NOT re-encoded from the decoded buffer — the\n * trailing bits of a 32-byte MAC are dropped by base64url decoding, so the\n * last character must be compared in string form). */\n mac: string\n}\n\nexport function parseLocalBearer(bearer: string): ParsedBearer | null {\n if (typeof bearer !== \"string\" || bearer.length > 256) return null\n const parts = bearer.split(\".\")\n if (parts.length !== 3 || parts[0] !== LOCAL_BEARER_PREFIX) return null\n const [, fingerprint, mac] = parts as [string, string, string]\n if (!/^[0-9a-f]{32}$/.test(fingerprint) || !/^[A-Za-z0-9_-]+$/.test(mac)) return null\n return { fingerprint, mac }\n}\n\n/** Constant-time MAC check. Runs the same HMAC + compare for an unknown\n * fingerprint (against a throwaway secret) so the miss path costs the same. */\nexport function verifyLocalBearer(rec: LocalDeviceRecord | undefined, parsed: ParsedBearer | null): boolean {\n const target = rec ?? { fingerprint: \"0\".repeat(32), secret: randomBytes(32).toString(\"base64url\") }\n const expected = macOf(target).toString(\"base64url\")\n const presented = parsed?.mac ?? \"\"\n const sameLength = presented.length === expected.length\n const equal = timingSafeEqual(\n Buffer.from(expected),\n Buffer.from(sameLength ? presented : \"0\".repeat(expected.length)),\n )\n return Boolean(rec) && parsed !== null && sameLength && equal\n}\n\nexport function isLocalDeviceRecord(v: unknown): v is LocalDeviceRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"fingerprint\"] === \"string\" &&\n /^[0-9a-f]{32}$/.test(r[\"fingerprint\"]) &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"createdAt\"] === \"string\" &&\n typeof r[\"secret\"] === \"string\" &&\n r[\"secret\"].length > 0\n )\n}\n","/**\n * Host-side pairing registry (design: DESIGN §3/§4, PLAN deliverable 2). Lives\n * in `@agentproto/pairing-host` so any AIP-59 host — the agentproto daemon is\n * one, a standalone app is another — can embed it; `@agentproto/runtime`\n * re-exports it unchanged.\n *\n * Owns everything a host needs to be *paired with* over an untrusted\n * rendezvous:\n *\n * - **Offer store** — in-memory, single-use, expiry-checked one-time offer\n * secrets minted by `pair offer`. A daemon restart voids outstanding offers\n * (acceptable + documented). The daemon parks on the offer's ROUTE token and\n * the handshake's `verifyAuthToken` predicate checks the sealed AUTH token,\n * SPENDING the offer on the first successful handshake (a wrong token spends\n * nothing).\n * - **Pairings store** — `~/.agentproto/pairings.json` (0600, atomic write),\n * one record per paired client: `{clientPub, name, fingerprint, createdAt,\n * lastSeen, pairRoot, rendezvousUrl}`. `pairRoot` is the long-term shared\n * secret (`derivePairRoot`) from which reconnect routing tokens derive.\n * - **Rendezvous connections** — for a fresh offer, and for every persisted\n * pairing on boot (autoconnect), the daemon dials the rendezvous *outbound*\n * (`side=daemon&t=<route>`), runs `daemonHandshakeOverSink` with the P1\n * crypto, and — on success — serves the spliced, E2E-wrapped channel exactly\n * like `serve --connect` serves a tunnel host. The serving path itself is\n * injected (`serve`) so this module stays free of pty/adapter concerns and\n * the CLI can reuse its `createTunnelServer` config verbatim.\n * - **Reconnect epochs** — a persisted pairing's standing connection parks on\n * the pairing-derived route `HKDF(pairRoot, \"rv-route\"‖epoch)`, rotated per\n * UTC day. The daemon parks on BOTH the current and previous\n * epoch tokens so a client whose clock straddles midnight still finds it;\n * reconnect-with-backoff keeps the standing connection alive.\n * - **Revocation** — removing a pairing drops its rendezvous connections, so\n * it can no longer be served. For a grace period (`revokedGraceMs`, default\n * 14 days) the daemon keeps a *tombstone*: it still parks on the pairing's\n * epoch ROUTES, completes the (daemon-signed) handshake only for a hello\n * carrying that epoch's AUTH token, and answers with a single E2E-encrypted\n * `error{code:\"pairing_revoked\"}` frame before closing — never serving the\n * channel. That is what lets a client tell \"unpaired\" from \"daemon\n * offline\" (which both otherwise look like a park timeout at the broker)\n * and stop retrying. The signal is authenticated like every tunnel frame:\n * it arrives inside the AEAD channel of a handshake whose transcript the\n * client verified against the pinned daemon key, so the broker can't forge\n * it; and the broker, which knows the routes, can't elicit it either (no\n * auth token). A tombstone keeps only the per-epoch route + auth tokens for\n * its window — the pair root is dropped at revoke.\n *\n * ## Reconnect authentication (why no stable client key)\n *\n * The handshake carries only a client *ephemeral* key (regenerated per\n * handshake) — there is no persistent client identity key. So a reconnect\n * authenticates the client by **possession of the epoch AUTH token**\n * `HKDF(pairRoot, \"rv-auth\"‖epoch)`, which derives from `pairRoot` — a secret\n * only the paired client and daemon share (it fell out of the original ECDH).\n * The daemon parks on the pairing's epoch ROUTE, and on that connection the\n * `verifyAuthToken` predicate accepts a hello whose sealed auth equals the same\n * epoch's auth token (constant-time). The route and auth are separate one-way\n * HKDF outputs: the broker sees every route and can't compute an auth from it.\n * (pair/v1 used ONE value for both, so a broker could replay a logged route as\n * the proof and be served.) The stored `fingerprint` (of the first handshake's\n * ephemeral key) is a stable display/revocation handle, not an auth input.\n *\n * ## Legacy (pair/v1) pairings\n *\n * A v1 `pairings.json` is loaded with every record flagged `legacy`: listed and\n * revocable, never served. The daemon still parks on a legacy pairing's routes\n * (the epoch route is unchanged from v1) so that pairing's not-yet-upgraded\n * client reaches it and is told to re-pair — see `respondToLegacyHandshake` —\n * rather than timing out. v1 records are not upgraded in place: v1 let the\n * broker pair as a client, so any v1 record may be the broker's own.\n */\n\n/*\n * ## Local devices (optional host feature)\n *\n * A loopback client (an MCP host on the same machine) has no use for a\n * rendezvous round-trip, yet should still be \"a paired device\": listed,\n * named, revocable. `mintLocalDevice({name})` creates one without a handshake:\n * - a record `{fingerprint, name, createdAt, secret}` (32 random bytes of\n * per-device `secret`) is appended to the OPTIONAL `localDevices` array of\n * `pairings.json` (still `v: 2`, still 0600; files without the key load\n * unchanged, and an older host that rewrites the file simply drops it,\n * which fails closed: those devices stop verifying).\n * - the bearer is `apd1.<fingerprint>.<HMAC-SHA256(secret, \"…|fingerprint\")>`\n * (base64url). It is never stored or logged; the host re-derives it.\n * - `verifyDeviceBearer(b)` looks the record up by fingerprint, compares the\n * MAC in constant time, and returns `{fingerprint, name}` or null. It stats\n * `pairings.json` on every call and reloads `localDevices` when the file\n * changed, so a `revoke` by another process (a CLI) takes effect on the\n * very next verify; nothing is cached past a revoke.\n * - local devices show in `list()` (flagged `local: true`, with empty\n * `clientPub`/`pairRoot`/`rendezvousUrl`; the secret is never returned),\n * `revoke()` and `rename()` work by fingerprint or name, no tombstone, no\n * rendezvous loop.\n */\n\nimport { randomBytes, createHmac, timingSafeEqual } from \"node:crypto\"\nimport { mkdir, readFile, writeFile, chmod, rename, stat } from \"node:fs/promises\"\nimport { homedir } from \"node:os\"\nimport { dirname, join, basename } from \"node:path\"\nimport {\n daemonHandshakeOverSink,\n type ErrorFrame,\n type FrameSink,\n type E2eFrameSink,\n} from \"@agentproto/acp/tunnel\"\nimport {\n decodePairingHello,\n encodePairingMessage,\n respondToHandshake,\n respondToLegacyHandshake,\n derivePairRoot,\n deriveEpochTokens,\n deriveOfferTokens,\n currentEpoch,\n encodeOfferUrl,\n HOSTED_RENDEZVOUS_URL,\n OFFER_VERSION,\n PAIRING_PROTOCOL_OUTDATED_MESSAGE,\n PairingError,\n type RouteAuthTokens,\n} from \"@agentproto/secrets/pairing\"\nimport {\n identityFingerprint,\n type DaemonIdentity,\n} from \"@agentproto/secrets/identity\"\nimport { createReconnectLogGate } from \"./reconnect-log-gate.js\"\nimport {\n deriveLocalBearer,\n isLocalDeviceRecord,\n newLocalDevice,\n parseLocalBearer,\n verifyLocalBearer,\n type LocalDeviceRecord,\n} from \"./local-device.js\"\n\n/** `pairings.json` format. v2 = pairings made under pair/v2 (route/auth\n * split). A v1 file still loads, every record flagged `legacy`. */\nexport const PAIRINGS_VERSION = 2 as const\nconst LEGACY_PAIRINGS_VERSION = 1\n\n/** One persisted pairing. Written verbatim to `pairings.json`. */\nexport interface PairingRecord {\n /** The client ephemeral public key from the FIRST handshake (a snapshot —\n * reconnects use fresh ephemerals; this is not an auth input). */\n clientPub: string\n /** Human-facing label the client chose on `pair accept`. */\n name: string\n /** `identityFingerprint(clientPub)` from the first handshake — the stable\n * display + revocation handle. */\n fingerprint: string\n /** ISO-8601 first-pair timestamp. */\n createdAt: string\n /** ISO-8601 of the most recent successful (re)connect. */\n lastSeen: string\n /** Long-term shared secret (base64), from which epoch routing tokens derive. */\n pairRoot: string\n /** Rendezvous endpoint to reconnect through. */\n rendezvousUrl: string\n /** Set on a pairing made under the retired pair/v1 protocol: listed and\n * revocable, never served — its client is told to re-pair. */\n legacy?: true\n /** Set when this pairing was made under an offer minted with\n * `agentproto pair offer --host` (PR-C) — i.e. the offering daemon's own\n * intent, recorded server-side at handshake time from the OfferEntry it\n * itself created, never from the (unauthenticated) `scope` param a client\n * presented in the offer URL. Absent for an ordinary remote-control\n * pairing. Purely informational here — it does not change how this\n * pairing is served. */\n scope?: \"host\"\n /** Set on a record returned by `list()` for a local device (see \"Local\n * devices\"): it has no rendezvous, `clientPub`/`pairRoot`/`rendezvousUrl`\n * are empty strings. Never written to the `pairings` array of the file. */\n local?: true\n}\n\n/** A revoked pairing still answered with `pairing_revoked` until `expiresAt`\n * (see \"Revocation\" above). Persisted in `pairings.json` under `revoked`. */\nexport interface RevokedPairingRecord {\n /** The revoked pairing's fingerprint. */\n fingerprint: string\n /** Its client label. */\n name: string\n /** Rendezvous endpoint its client reconnects through. */\n rendezvousUrl: string\n /** ISO-8601 revocation time. */\n revokedAt: string\n /** ISO-8601 end of the grace window; the tombstone is dropped after it. */\n expiresAt: string\n /** The epoch route (parked on) + auth (verified) tokens covering the window\n * (previous epoch at revoke time through the epoch of `expiresAt`) —\n * derived from the pair root at revoke time so the root itself need not be\n * kept. */\n routes: ({ epoch: number } & RouteAuthTokens)[]\n}\n\ninterface PairingsFile {\n v: typeof PAIRINGS_VERSION | typeof LEGACY_PAIRINGS_VERSION\n pairings: PairingRecord[]\n /** Tombstones of revoked pairings (absent in files written before them;\n * ignored by daemons that predate them). */\n revoked?: RevokedPairingRecord[]\n /** Local-device credentials (see \"Local devices\"). Optional: absent in files\n * written before them. */\n localDevices?: LocalDeviceRecord[]\n}\n\n/** The tunnel `error` code a revoked pairing's client receives. */\nexport const PAIRING_REVOKED_CODE = \"pairing_revoked\" as const\n\nfunction revokedFrame(): ErrorFrame {\n return {\n t: \"error\",\n code: PAIRING_REVOKED_CODE,\n message: \"this device was unpaired from the daemon (agentproto pair revoke); pair again with a new offer\",\n }\n}\n\n/** Mode of a served channel — first-contact offer vs. an established reconnect. */\nexport type PairingChannelMode = \"offer\" | \"reconnect\"\n\n/** Context handed to the injected `serve` when a channel comes up. */\nexport interface PairingChannelContext {\n mode: PairingChannelMode\n /** Stable pairing fingerprint (present for reconnect; for an offer it's the\n * freshly-derived one). */\n fingerprint: string\n /** Client label. */\n name: string\n /** This pairing's OWN recorded scope (mirrors `PairingRecord.scope`) —\n * `\"host\"` when the offer that produced it was minted with\n * `agentproto pair offer --host`, absent for an ordinary remote-control\n * pairing. Present on BOTH the offer and every reconnect context, so\n * `serve` can gate scope-restricted routes (e.g. device-inference\n * proxying) the same way on first contact and on every later reconnect. */\n scope?: \"host\"\n}\n\n/** Handle to a live served channel — the registry closes it on teardown. */\nexport interface PairingChannelHandle {\n close(): Promise<void>\n}\n\nexport interface CreatedOffer {\n /** The one-time offer secret (base64url) — the URL's `s`. Never on the wire:\n * the broker only ever sees its derived route. */\n secret: string\n /** Offer expiry, unix seconds. */\n exp: number\n /** The full `agentproto://pair?…` offer URL. */\n url: string\n /** The daemon's identity fingerprint (shown to the human at accept time). */\n fingerprint: string\n /** The rendezvous the offer routes through. */\n rendezvousUrl: string\n /** True when `rendezvousUrl` is the hosted default — i.e. neither an explicit\n * `--rendezvous` nor a configured `pairing.rendezvous` applied, so the offer\n * falls back to `HOSTED_RENDEZVOUS_URL`. Surfaced so `pair offer` can flag,\n * never silently, that the daemon is relaying through our infrastructure. */\n rendezvousIsHostedDefault: boolean\n /** Echoes `CreateOfferInput.scope`. `\"host\"` when minted with `--host`;\n * absent for a plain offer. */\n scope?: \"host\"\n}\n\nexport interface CreateOfferInput {\n /** Time-to-live in ms. Default 10 minutes. */\n ttlMs?: number\n /** Rendezvous URL override; falls back to the configured default. */\n rendezvousUrl?: string\n /** Mint a HOST-scoped offer (PR-C): the accepting side may register this\n * daemon as a driveable host (`devices add`), not just remote-control it.\n * Recorded on the resulting `OfferEntry`/`PairingRecord` server-side — this\n * is the authoritative record of what was granted, independent of the\n * `scope` param an accepting client's offer URL happens to carry. */\n scope?: \"host\"\n}\n\nexport interface PairingRegistryDeps {\n /** Lazily load (create on first use) the daemon identity. Kept lazy so a\n * daemon that never pairs never writes an identity file. */\n loadIdentity: () => Promise<DaemonIdentity>\n /** Path to `pairings.json`. Defaults to `~/.agentproto/pairings.json`. */\n pairingsPath?: string\n /** Configured rendezvous URL (from `config.pairing.rendezvous`). Three states:\n * - `undefined` (key absent) → `createOffer` falls back to the hosted default\n * (`HOSTED_RENDEZVOUS_URL`).\n * - a non-empty URL → that endpoint is the default for offers.\n * - `\"\"` (explicitly empty) → an explicit opt-out: no default applies, so an\n * offer without `--rendezvous` fails closed. Wire it through verbatim (do\n * not coerce `\"\"` to `undefined`) or the opt-out silently reverts to the\n * hosted default. */\n defaultRendezvousUrl?: string\n /** Dial a rendezvous WS and adapt it to a FrameSink. Injected by the CLI\n * (uses `ws` + `wrapWebSocket`). Rejects if the dial fails. The `signal`\n * aborts on registry shutdown. */\n dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>\n /** Serve the E2E-wrapped channel (the CLI builds `createTunnelServer` over\n * it with the same config `serve --connect` uses). */\n serve: (sink: E2eFrameSink, ctx: PairingChannelContext) => PairingChannelHandle\n /** Injectable clock (ms). Defaults to Date.now. */\n now?: () => number\n /** Diagnostic log sink. */\n log?: (line: string) => void\n /** Reconnect backoff floor / ceiling (ms). Defaults 1s / 30s. */\n reconnectMinMs?: number\n reconnectMaxMs?: number\n /** How long a parked daemon socket waits for a client hello before recycling\n * the connection. Kept just under the broker's park timeout. Default 110s. */\n handshakeTimeoutMs?: number\n /** How long a revoked pairing is still answered with `pairing_revoked` (see\n * \"Revocation\" above). Default 14 days; `0` disables tombstones, so a\n * revoked client just stops finding the daemon. */\n revokedGraceMs?: number\n}\n\nexport interface PairingRegistry {\n /** Mint a one-time offer + start dialing the rendezvous for it. */\n createOffer(input?: CreateOfferInput): Promise<CreatedOffer>\n /** All persisted pairings (copies). Loads `pairings.json` on first call. */\n list(): Promise<PairingRecord[]>\n /** Rename a pairing by fingerprint or name (cosmetic only — no effect on\n * auth or routing). Returns false when nothing matched; throws on an\n * empty `newName`. */\n rename(idOrName: string, newName: string): Promise<boolean>\n /** Drop a pairing by fingerprint or name; stops its rendezvous connections.\n * Returns false when nothing matched. */\n revoke(idOrName: string): Promise<boolean>\n /** Is a channel (offer or reconnect) currently served for this fingerprint? */\n isOnline(fingerprint: string): boolean\n /** Start standing reconnect connections for every persisted pairing. Call\n * after the gateway is up (so the injected `serve` can reach it). */\n startAutoconnect(): Promise<void>\n /** Tear down every offer + reconnect connection and served channel. */\n shutdown(): Promise<void>\n}\n\n/** What `mintLocalDevice` hands back. `bearer` is shown ONCE to the caller\n * (write it to the client's config); the host never logs or stores it. */\nexport interface LocalDeviceCredential {\n bearer: string\n fingerprint: string\n name: string\n createdAt: string\n}\n\n/** The registry `createPairingRegistry` returns: the daemon-facing\n * `PairingRegistry` plus the optional local-device credential. */\nexport interface PairingHostRegistry extends PairingRegistry {\n /** Mint a paired-device record for a loopback client and return its bearer.\n * Visible in `list()`, revocable with `revoke()`. Throws on an empty name. */\n mintLocalDevice(input: { name: string }): Promise<LocalDeviceCredential>\n /** Constant-time check of a device bearer. `null` for anything unknown,\n * malformed or revoked; a revoke is honoured on the very next call. */\n verifyDeviceBearer(bearer: string): Promise<{ fingerprint: string; name: string } | null>\n}\n\nconst DEFAULT_TTL_MS = 10 * 60_000\nconst DEFAULT_RECONNECT_MIN_MS = 1_000\nconst DEFAULT_RECONNECT_MAX_MS = 30_000\nconst DEFAULT_HANDSHAKE_TIMEOUT_MS = 110_000\nconst DEFAULT_REVOKED_GRACE_MS = 14 * 86_400_000\n\nfunction defaultPairingsPath(): string {\n return join(homedir(), \".agentproto\", \"pairings.json\")\n}\n\n/**\n * Read-only snapshot of `pairings.json` for callers that just want the\n * persisted records — no `dial`/`serve`/`loadIdentity` deps, no rendezvous\n * connections started (e.g. `agentproto doctor`'s devices check). Mirrors\n * `ensureLoaded`'s parse + v1-legacy flagging; never throws — a missing or\n * malformed file just yields `[]`.\n */\nexport async function readPairingsSnapshot(path?: string): Promise<PairingRecord[]> {\n try {\n const raw = await readFile(path ?? defaultPairingsPath(), \"utf8\")\n const parsed: unknown = JSON.parse(raw)\n if (!isPairingsFile(parsed)) return []\n const fromV1 = parsed.v === LEGACY_PAIRINGS_VERSION\n return parsed.pairings.map(rec => (fromV1 ? { ...rec, legacy: true } : rec))\n } catch {\n return []\n }\n}\n\nfunction b64url(bytes: Buffer): string {\n return bytes.toString(\"base64\").replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\")\n}\n\n/** Constant-time string compare (HMAC-blind so length never leaks). Mirrors the\n * rendezvous/relay `timing-safe` pattern; inlined so runtime gains no dep. */\nfunction constantTimeEqual(a: string, b: string): boolean {\n const key = randomBytes(32)\n const da = createHmac(\"sha256\", key).update(a, \"utf8\").digest()\n const db = createHmac(\"sha256\", key).update(b, \"utf8\").digest()\n return timingSafeEqual(da, db)\n}\n\ninterface OfferEntry {\n /** Broker route derived from the offer secret (the map key). */\n route: string\n /** Auth token derived from the offer secret; the sealed hello must carry it. */\n auth: string\n exp: number // unix seconds\n spent: boolean\n rendezvousUrl: string\n /** This daemon's own recorded intent for the offer it minted (PR-C) — set\n * by `createOffer`'s caller, never by anything a client's hello claims\n * (the hello has no scope field). Written onto the resulting\n * `PairingRecord` on success. */\n scope?: \"host\"\n}\n\n/** Re-pair notice sent (E2E, inside a completed v1 channel) to a pair/v1\n * client. A v1 client drops tunnel-level `error` frames and only surfaces a\n * `hello` whose `version` it doesn't speak — as \"Tunnel daemon speaks\n * <version>; client speaks …\" — so the notice rides in that field too. */\nconst OUTDATED_HELLO_VERSION = `pair/v2 required — ${PAIRING_PROTOCOL_OUTDATED_MESSAGE}`\n\n/** One managed rendezvous connection loop (offer or a single epoch). */\ninterface ConnectionLoop {\n abort: AbortController\n /** Resolves when the loop has fully stopped. */\n done: Promise<void>\n}\n\nexport function createPairingRegistry(deps: PairingRegistryDeps): PairingHostRegistry {\n const pairingsPath = deps.pairingsPath ?? defaultPairingsPath()\n const now = deps.now ?? Date.now\n const log = deps.log ?? (() => {})\n const reconnectMinMs = deps.reconnectMinMs ?? DEFAULT_RECONNECT_MIN_MS\n const reconnectMaxMs = deps.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS\n const handshakeTimeoutMs = deps.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const revokedGraceMs = deps.revokedGraceMs ?? DEFAULT_REVOKED_GRACE_MS\n\n // Rate-limit dial-failure logging per loop key. A revoked/offline peer's\n // standing reconnect re-dials forever; without this a single dead pairing\n // buries daemon.log (it was ~85% of one 2.9MB log). First failure logs at\n // once, then at most one line per window carrying the suppressed count;\n // a successful dial resets the key. Backoff/timing is unchanged.\n const dialFailureGate = createReconnectLogGate({ now })\n\n // Rate-limit handshake-failure logging per loop key, same rationale as\n // `dialFailureGate`: a flap (peer closing mid-handshake, in bursts during an\n // E2E channel outage) re-dials on backoff forever, and an un-gated line per\n // attempt buries daemon.log. Unlike a dial failure, a HANDSHAKE failure\n // means the peer was actually reached — the step it died at (pre-hello\n // transport close vs. hello timeout vs. rejected token) is the diagnostic,\n // so the gated line names it via the error message itself.\n const handshakeFailureGate = createReconnectLogGate({ now })\n\n // Same gate shape for the POST-reply close reason surfaced by\n // `daemonHandshakeOverSink`'s log hook — the step the observed flaps break\n // at (Noise OK, reply sent, remote hangs up before/around the first\n // forwarded exchange).\n const handshakeCloseGate = createReconnectLogGate({ now })\n\n /** fingerprint → record. Source of truth in memory; disk is the mirror. */\n const pairings = new Map<string, PairingRecord>()\n /** fingerprint → count of channels currently served (offer or reconnect).\n * Never persisted — a fresh boot starts with nothing online until a\n * reconnect loop pairs. */\n const onlineCounts = new Map<string, number>()\n /** fingerprint → tombstone of a revoked pairing (see \"Revocation\"). */\n const revoked = new Map<string, RevokedPairingRecord>()\n /** route → offer. */\n const offers = new Map<string, OfferEntry>()\n /** Loops keyed for lifecycle: `offer:<route>` and `pair:<fp>:<slot>`. Keys\n * reach the log, so they carry the (public) route, never a secret. */\n const loops = new Map<string, ConnectionLoop>()\n /** Live served channels, so shutdown/revoke can close them. */\n const channels = new Set<PairingChannelHandle>()\n\n /** fingerprint → local-device record (with its secret). */\n const localDevices = new Map<string, LocalDeviceRecord>()\n /** Identity (mtime/size/inode) of `pairings.json` as last read or written by\n * this registry; a mismatch means another process changed it. */\n let fileSig = \"\"\n\n let shuttingDown = false\n\n async function sigOfFile(): Promise<string | null> {\n try {\n const st = await stat(pairingsPath)\n return `${st.mtimeMs}:${st.size}:${st.ino}`\n } catch (err) {\n if (isEnoent(err)) return null\n throw err\n }\n }\n\n function replaceLocalDevices(records: unknown): void {\n localDevices.clear()\n if (!Array.isArray(records)) return\n for (const rec of records) if (isLocalDeviceRecord(rec)) localDevices.set(rec.fingerprint, rec)\n }\n\n /** Re-read `localDevices` if another process rewrote `pairings.json`. Only\n * the local-device map is replaced; pairings stay as this registry holds\n * them. A vanished file clears the map (fail closed); an unreadable one\n * keeps the previous view. */\n async function refreshLocalDevices(): Promise<void> {\n let sig: string | null\n try {\n sig = await sigOfFile()\n } catch {\n return\n }\n if (sig === null) {\n localDevices.clear()\n fileSig = \"\"\n return\n }\n if (sig === fileSig) return\n try {\n const parsed: unknown = JSON.parse(await readFile(pairingsPath, \"utf8\"))\n if (isPairingsFile(parsed)) {\n replaceLocalDevices(parsed.localDevices)\n fileSig = sig\n }\n } catch {\n /* mid-write or corrupt: keep the last good view */\n }\n }\n\n // Pairings are loaded lazily (ensureLoaded) on the first list/offer/autoconnect\n // so construction stays synchronous and touches no disk. One shared promise,\n // so a concurrent second caller waits for the load instead of seeing an\n // empty registry.\n let loadPromise: Promise<void> | null = null\n function ensureLoaded(): Promise<void> {\n loadPromise ??= doLoad()\n return loadPromise\n }\n async function doLoad(): Promise<void> {\n try {\n const sig = await sigOfFile()\n const raw = await readFile(pairingsPath, \"utf8\")\n const parsed: unknown = JSON.parse(raw)\n if (isPairingsFile(parsed)) {\n if (sig !== null) fileSig = sig\n replaceLocalDevices(parsed.localDevices)\n const fromV1 = parsed.v === LEGACY_PAIRINGS_VERSION\n for (const rec of parsed.pairings) {\n pairings.set(rec.fingerprint, fromV1 ? { ...rec, legacy: true } : rec)\n }\n for (const rec of parsed.revoked ?? []) {\n if (isRevokedRecord(rec) && tombstoneLive(rec)) revoked.set(rec.fingerprint, rec)\n }\n const legacy = Array.from(pairings.values()).filter(r => r.legacy)\n if (legacy.length > 0) {\n log(\n `[pairing] ${legacy.length} pairing(s) in ${pairingsPath} use the retired pair/v1 ` +\n `protocol and will not be served (${legacy.map(r => r.name).join(\", \")}). ` +\n \"Re-pair each client — run `agentproto pair offer` — then \" +\n \"`agentproto pair revoke <name>` the legacy entry.\",\n )\n }\n } else {\n log(`[pairing] ignoring ${pairingsPath}: unrecognised format`)\n }\n } catch (err) {\n if (!isEnoent(err)) {\n log(`[pairing] could not read ${pairingsPath}: ${errMsg(err)}`)\n }\n }\n }\n\n // Serialize persist() calls through one chain: two calls racing (e.g. a\n // reconnect's `lastSeen` update against a `rename`/`revoke` a user just\n // triggered) would otherwise share the same pid-keyed tmp filename AND —\n // worse — could finish out of start order, so whichever rename() landed\n // last would silently overwrite newer in-memory state with a stale\n // snapshot. Chaining makes each write wait its turn and read `pairings`/\n // `revoked` fresh at that turn, so writes land in call order and every\n // update is durably persisted.\n let persistChain: Promise<void> = Promise.resolve()\n function persist(): Promise<void> {\n const run = persistChain.then(() => doPersist())\n persistChain = run.catch(() => {})\n return run\n }\n /** Apply a local-device change and write it, serialized with every other\n * write: refresh from disk first so a change another process made is not\n * clobbered by this registry's stale view. */\n function mutateLocal(apply: () => void): Promise<void> {\n const run = persistChain.then(async () => {\n await refreshLocalDevices()\n apply()\n await doPersist(false)\n })\n persistChain = run.catch(() => {})\n return run\n }\n async function doPersist(refresh = true): Promise<void> {\n if (refresh) await refreshLocalDevices()\n for (const [fp, rec] of revoked) if (!tombstoneLive(rec)) revoked.delete(fp)\n const file: PairingsFile = {\n v: PAIRINGS_VERSION,\n pairings: Array.from(pairings.values()),\n ...(revoked.size > 0 ? { revoked: Array.from(revoked.values()) } : {}),\n ...(localDevices.size > 0 ? { localDevices: Array.from(localDevices.values()) } : {}),\n }\n const dir = dirname(pairingsPath)\n await mkdir(dir, { recursive: true })\n const tmp = join(dir, `.${basename(pairingsPath)}.tmp-${process.pid}`)\n await writeFile(tmp, JSON.stringify(file, null, 2) + \"\\n\", { encoding: \"utf8\", mode: 0o600 })\n await chmod(tmp, 0o600).catch(() => {})\n await rename(tmp, pairingsPath)\n fileSig = (await sigOfFile()) ?? \"\"\n }\n\n // ── connection loop ────────────────────────────────────────────\n\n interface LoopSpec {\n key: string\n rendezvousUrl: string\n /** Route (dialled) + auth (verified) tokens for this iteration —\n * recomputed for epoch loops; the derivation is async. */\n tokens: () => RouteAuthTokens | Promise<RouteAuthTokens>\n /** Verify the auth token presented in the sealed hello against this\n * iteration's `tokens()`. Must be constant-time. */\n verify: (presented: string, expected: RouteAuthTokens) => boolean\n /** Whether to stop after the first successful serve (offers) vs. loop\n * forever re-parking (reconnect). */\n singleUse: boolean\n /** Should the loop keep running? (e.g. offer not yet expired/spent, or the\n * pairing still exists). Checked at the top of each iteration. */\n shouldContinue: () => boolean\n /** Set for a revoked pairing's tombstone: after the handshake, send this\n * frame and close instead of serving (`onPaired` is not called). */\n reject?: ErrorFrame\n /** Build the serve context + persist side-effects on a successful pair. */\n onPaired: (\n session: import(\"@agentproto/secrets/pairing\").PairingSession,\n hello: import(\"@agentproto/secrets/pairing\").PairingHello,\n ) => Promise<PairingChannelContext>\n }\n\n function startLoop(spec: LoopSpec): void {\n if (loops.has(spec.key)) return\n const abort = new AbortController()\n const done = runLoop(spec, abort.signal).catch(err => {\n log(`[pairing] loop ${spec.key} ended: ${errMsg(err)}`)\n })\n loops.set(spec.key, { abort, done })\n }\n\n async function stopLoop(key: string, reason?: typeof REVOKED): Promise<void> {\n const loop = loops.get(key)\n if (!loop) return\n loops.delete(key)\n loop.abort.abort(reason)\n await loop.done.catch(() => {})\n }\n\n async function runLoop(spec: LoopSpec, signal: AbortSignal): Promise<void> {\n let backoff = reconnectMinMs\n while (!signal.aborted && spec.shouldContinue()) {\n const expected = await spec.tokens()\n let sink: FrameSink\n try {\n sink = await deps.dial(dialUrl(spec.rendezvousUrl, expected.route), signal)\n } catch (err) {\n if (signal.aborted) break\n const line = dialFailureGate.onFailure(\n spec.key,\n `[pairing] dial ${spec.key} failed: ${errMsg(err)}`,\n )\n if (line) log(line)\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n // A socket the broker refused on arrival (e.g. `token_in_use` while the\n // previous splice on this route is still tearing down) can come back\n // already closed: its close event fired before we could subscribe, so the\n // handshake would sit out its whole timeout. Treat it as a failed dial.\n if (!sink.isOpen) {\n if (signal.aborted) break\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n // Dial succeeded — clear any accumulated suppression for this key so the\n // next outage logs its first failure promptly.\n dialFailureGate.onSuccess(spec.key)\n\n // Make the in-flight handshake abortable: closing the sink on shutdown\n // makes `daemonHandshakeOverSink` reject promptly (transport closed),\n // instead of blocking on its own timeout for up to handshakeTimeoutMs.\n let serving = false\n const onAbort = (): void => {\n // A revoke of a live channel is handled by waitClosed, which tells the\n // client before closing — closing the raw socket here would drop that.\n if (serving && signal.reason === REVOKED) return\n try {\n sink.close(\"registry shutdown\")\n } catch {\n /* already closing */\n }\n }\n signal.addEventListener(\"abort\", onAbort, { once: true })\n\n try {\n let capturedSession: import(\"@agentproto/secrets/pairing\").PairingSession | null = null\n let capturedHello: import(\"@agentproto/secrets/pairing\").PairingHello | null = null\n let legacyPeer = false\n let wrapped: E2eFrameSink\n try {\n const identity = await deps.loadIdentity()\n wrapped = await daemonHandshakeOverSink(\n sink,\n async helloBytes => {\n let hello: import(\"@agentproto/secrets/pairing\").PairingHello\n try {\n hello = decodePairingHello(helloBytes)\n } catch (err) {\n if (!(err instanceof PairingError) || err.code !== \"pairing_protocol_outdated\") throw err\n // A pair/v1 client: complete ITS handshake only to tell it to\n // re-pair. No auth is checked and nothing is served.\n legacyPeer = true\n return respondToLegacyHandshake(helloBytes, identity)\n }\n const result = await respondToHandshake(hello, {\n identity,\n verifyAuthToken: presented => spec.verify(presented, expected),\n })\n capturedSession = result.session\n capturedHello = hello\n return { reply: encodePairingMessage(result.reply), keys: result.session }\n },\n {\n timeoutMs: handshakeTimeoutMs,\n // Post-handshake close reason (BOOTSTRAP P4 item 2, field\n // evidence: the flaps break AFTER the reply is sent, and the\n // reason was captured at this exact layer and discarded).\n // Gated like the handshake-failure log so a flap's retry loop\n // doesn't bury daemon.log.\n log: line => {\n const gated = handshakeCloseGate.onFailure(spec.key, `[pairing] ${spec.key}: ${line}`)\n if (gated) log(gated)\n },\n },\n )\n } catch (err) {\n if (signal.aborted) break\n // A park timeout, a rejected offer, or a tampered hello. Fail closed\n // and re-dial after a short backoff (park timeouts are common + benign).\n // Gate the log like a dial failure (bursts during a flap must not\n // bury daemon.log) but still NAME the step: the error message from\n // `daemonHandshakeOverSink` distinguishes a transport close\n // mid-handshake (`transport closed during handshake: <reason>`) from\n // a hello timeout — exactly what the next channel flap needs to\n // self-explain on this end.\n const line = handshakeFailureGate.onFailure(\n spec.key,\n `[pairing] handshake for ${spec.key} failed: ${errMsg(err)}`,\n )\n if (line) log(line)\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n handshakeFailureGate.onSuccess(spec.key)\n handshakeCloseGate.onSuccess(spec.key)\n\n if (legacyPeer) {\n sendOutdatedNotice(wrapped)\n log(`[pairing] told a pair/v1 client on ${spec.key} to re-pair`)\n // Not a success: keep backing off so a v1 client (or anyone replaying\n // a v1 hello) can't spin this loop.\n await sleep(backoff, signal)\n backoff = Math.min(backoff * 2, reconnectMaxMs)\n continue\n }\n\n backoff = reconnectMinMs // success resets\n\n if (!capturedSession || !capturedHello) {\n wrapped.close(\"internal: missing session\")\n continue\n }\n\n if (spec.reject) {\n // Tombstone: the client proved possession of a revoked pairing's\n // epoch AUTH token and verified our signature — tell it, inside the\n // AEAD channel, that it is unpaired. Never served. `close` flushes\n // the frame first.\n wrapped.send(spec.reject)\n wrapped.close(spec.reject.code)\n log(`[pairing] refused revoked pairing via ${spec.key}`)\n continue\n }\n\n let ctx: PairingChannelContext\n try {\n ctx = await spec.onPaired(capturedSession, capturedHello)\n } catch (err) {\n log(`[pairing] persist for ${spec.key} failed: ${errMsg(err)}`)\n wrapped.close(\"persist failed\")\n continue\n }\n\n serving = true\n const handle = deps.serve(wrapped, ctx)\n channels.add(handle)\n onlineCounts.set(ctx.fingerprint, (onlineCounts.get(ctx.fingerprint) ?? 0) + 1)\n log(`[pairing] channel up (${ctx.mode}) for ${ctx.fingerprint} via ${spec.key}`)\n\n const closeReason = await waitClosed(wrapped, signal)\n channels.delete(handle)\n await handle.close().catch(() => {})\n const remaining = (onlineCounts.get(ctx.fingerprint) ?? 1) - 1\n if (remaining > 0) onlineCounts.set(ctx.fingerprint, remaining)\n else onlineCounts.delete(ctx.fingerprint)\n // The close reason is the diagnostic when a channel dies right after\n // coming up (the observed E2E flap: Noise OK, then the connection\n // breaks before/at the first forwarded request) — name it when the\n // remote told us why, \"remote closed without a reason\" otherwise.\n log(\n `[pairing] channel closed (${ctx.mode}) for ${ctx.fingerprint}${\n closeReason !== undefined ? `: ${closeReason}` : \" (remote closed without a reason)\"\n }`,\n )\n\n if (spec.singleUse) break\n } finally {\n signal.removeEventListener(\"abort\", onAbort)\n }\n }\n }\n\n // ── offer handling ─────────────────────────────────────────────\n\n async function createOffer(input: CreateOfferInput = {}): Promise<CreatedOffer> {\n await ensureLoaded()\n // Precedence (PLAN deliverable 2): `--rendezvous` flag → `pairing.rendezvous`\n // in config → hosted default. A configured empty string is an explicit\n // opt-out (deliverable 4b): no default applies, so an offer without an\n // explicit `--rendezvous` fails closed — the reachable form of the branch\n // that used to fire whenever nothing was configured.\n let rendezvousUrl: string\n let rendezvousIsHostedDefault = false\n if (input.rendezvousUrl) {\n rendezvousUrl = input.rendezvousUrl\n } else if (deps.defaultRendezvousUrl !== undefined) {\n if (deps.defaultRendezvousUrl === \"\") {\n throw new PairingError(\n \"malformed_offer\",\n 'rendezvous disabled — pairing.rendezvous is set to \"\" (explicit opt-out); ' +\n \"pass --rendezvous to route this offer through a specific broker\",\n )\n }\n rendezvousUrl = deps.defaultRendezvousUrl\n } else {\n rendezvousUrl = HOSTED_RENDEZVOUS_URL\n rendezvousIsHostedDefault = true\n }\n const identity = await deps.loadIdentity()\n const secret = b64url(randomBytes(16))\n const { route, auth } = await deriveOfferTokens(secret)\n const ttlMs = input.ttlMs ?? DEFAULT_TTL_MS\n const exp = Math.floor((now() + ttlMs) / 1000)\n const scope = input.scope\n offers.set(route, { route, auth, exp, spent: false, rendezvousUrl, ...(scope ? { scope } : {}) })\n\n const fingerprint = await identityFingerprint(identity.x25519.pub)\n const url = encodeOfferUrl({\n v: OFFER_VERSION,\n rendezvousUrl,\n fingerprint,\n daemonX25519Pub: identity.x25519.pub,\n daemonEd25519Pub: identity.ed25519.pub,\n secret,\n exp,\n ...(scope ? { scope } : {}),\n })\n\n startOfferLoop({ route, auth }, rendezvousUrl)\n log(\n `[pairing] offer minted (exp ${new Date(exp * 1000).toISOString()}) via ${rendezvousUrl}` +\n (rendezvousIsHostedDefault ? \" (hosted default)\" : \"\") +\n (scope === \"host\" ? \" [HOST-SCOPED]\" : \"\"),\n )\n return { secret, exp, url, fingerprint, rendezvousUrl, rendezvousIsHostedDefault, ...(scope ? { scope } : {}) }\n }\n\n function offerValid(route: string): boolean {\n const entry = offers.get(route)\n return !!entry && !entry.spent && entry.exp * 1000 > now()\n }\n\n function startOfferLoop(tokens: RouteAuthTokens, rendezvousUrl: string): void {\n const { route } = tokens\n startLoop({\n key: `offer:${route}`,\n rendezvousUrl,\n tokens: () => tokens,\n shouldContinue: () => offerValid(route),\n singleUse: true,\n verify: presented => {\n const entry = offers.get(route)\n if (!entry || entry.spent || entry.exp * 1000 <= now()) return false\n // The AUTH token, never the route: the broker knows the route.\n if (!constantTimeEqual(presented, entry.auth)) return false\n entry.spent = true // SPEND — single use, and only on a correct auth\n return true\n },\n onPaired: async (session, hello) => {\n const pairRoot = await derivePairRoot(session)\n const fingerprint = session.peerFingerprint\n const nowIso = new Date(now()).toISOString()\n // The offer's OWN recorded scope — never anything the client's hello\n // claims (it has no scope field at all). `entry` is this offer's\n // OfferEntry, looked up by route, which `verify` already confirmed\n // above (it only returns true for a live, unspent, matching entry).\n const entry = offers.get(route)\n const record: PairingRecord = {\n clientPub: hello.ePub,\n name: session.clientName ?? fingerprint,\n fingerprint,\n createdAt: pairings.get(fingerprint)?.createdAt ?? nowIso,\n lastSeen: nowIso,\n pairRoot,\n rendezvousUrl,\n ...(entry?.scope ? { scope: entry.scope } : {}),\n }\n pairings.set(fingerprint, record)\n revoked.delete(fingerprint)\n await persist()\n // Start standing reconnect connections so the client can come back.\n startReconnectLoops(record)\n return { mode: \"offer\", fingerprint, name: record.name, ...(record.scope ? { scope: record.scope } : {}) }\n },\n })\n }\n\n // ── reconnect handling ─────────────────────────────────────────\n\n function startReconnectLoops(record: PairingRecord): void {\n // Two standing connections — current + previous epoch — so a client whose\n // clock sits on either side of the UTC-day boundary can still splice.\n for (const slot of [\"cur\", \"prev\"] as const) {\n const epochOf = () => (slot === \"cur\" ? currentEpoch(now()) : currentEpoch(now()) - 1)\n startLoop({\n key: `pair:${record.fingerprint}:${slot}`,\n rendezvousUrl: record.rendezvousUrl,\n tokens: () => deriveEpochTokens(record.pairRoot, epochOf()),\n shouldContinue: () => pairings.has(record.fingerprint),\n singleUse: false,\n // A legacy (v1) pairing is parked on only so its client can be told to\n // re-pair; it never authenticates, whatever it presents.\n verify: (presented, expected) =>\n !record.legacy && constantTimeEqual(presented, expected.auth),\n onPaired: async (session, _hello) => {\n const existing = pairings.get(record.fingerprint)\n if (existing) {\n existing.lastSeen = new Date(now()).toISOString()\n await persist().catch(err => log(`[pairing] lastSeen persist failed: ${errMsg(err)}`))\n }\n // Silence unused-session lint while keeping the signature uniform.\n void session\n return {\n mode: \"reconnect\",\n fingerprint: record.fingerprint,\n name: existing?.name ?? record.name,\n ...(record.scope ? { scope: record.scope } : {}),\n }\n },\n })\n }\n }\n\n function startTombstoneLoops(rec: RevokedPairingRecord): void {\n for (const slot of [\"cur\", \"prev\"] as const) {\n const epochOf = () => (slot === \"cur\" ? currentEpoch(now()) : currentEpoch(now()) - 1)\n const tokensFor = () => rec.routes.find(r => r.epoch === epochOf())\n startLoop({\n key: `revoked:${rec.fingerprint}:${slot}`,\n rendezvousUrl: rec.rendezvousUrl,\n tokens: () => tokensFor() ?? { route: \"\", auth: \"\" },\n shouldContinue: () =>\n revoked.get(rec.fingerprint) === rec && tombstoneLive(rec) && tokensFor() !== undefined,\n singleUse: false,\n // The AUTH token, never the route: the broker knows every route, and a\n // tombstone must not become a way for it to probe revocation state.\n verify: (presented, expected) => expected.auth !== \"\" && constantTimeEqual(presented, expected.auth),\n reject: revokedFrame(),\n onPaired: async () => {\n throw new Error(\"unreachable: a tombstone never serves\")\n },\n })\n }\n }\n\n function tombstoneLive(rec: RevokedPairingRecord): boolean {\n return Date.parse(rec.expiresAt) > now()\n }\n\n async function startAutoconnect(): Promise<void> {\n await ensureLoaded()\n for (const record of pairings.values()) {\n startReconnectLoops(record)\n }\n for (const rec of revoked.values()) {\n startTombstoneLoops(rec)\n }\n if (pairings.size > 0) {\n log(`[pairing] autoconnect: standing connections for ${pairings.size} pairing(s)`)\n }\n }\n\n // ── revocation ─────────────────────────────────────────────────\n\n async function revoke(idOrName: string): Promise<boolean> {\n await ensureLoaded()\n let target: PairingRecord | undefined = pairings.get(idOrName)\n if (!target) {\n for (const rec of pairings.values()) {\n if (rec.name === idOrName) {\n target = rec\n break\n }\n }\n }\n if (!target) {\n await refreshLocalDevices()\n const device = findLocalDevice(idOrName)\n if (!device) return false\n // No tombstone, no loops: a local device never rendezvoused. The map is\n // updated before this resolves, so the next verify already rejects it.\n await mutateLocal(() => void localDevices.delete(device.fingerprint))\n log(`[pairing] revoked local device ${device.fingerprint} (${device.name})`)\n return true\n }\n pairings.delete(target.fingerprint)\n let tombstone: RevokedPairingRecord | null = null\n // No tombstone for a legacy (v1) pairing: its client can only be told to\n // re-pair (see \"Legacy\"), which a v1 client can't act on as \"revoked\".\n if (revokedGraceMs > 0 && !target.legacy) {\n const at = now()\n const expiresAt = at + revokedGraceMs\n const routes: RevokedPairingRecord[\"routes\"] = []\n for (let e = currentEpoch(at) - 1; e <= currentEpoch(expiresAt); e++) {\n routes.push({ epoch: e, ...(await deriveEpochTokens(target.pairRoot, e)) })\n }\n tombstone = {\n fingerprint: target.fingerprint,\n name: target.name,\n rendezvousUrl: target.rendezvousUrl,\n revokedAt: new Date(at).toISOString(),\n expiresAt: new Date(expiresAt).toISOString(),\n routes,\n }\n revoked.set(target.fingerprint, tombstone)\n }\n await persist()\n // Stop serving/parking on this pairing's tokens. A live channel is told\n // `pairing_revoked` before it closes (see waitClosed).\n await stopLoop(`pair:${target.fingerprint}:cur`, REVOKED)\n await stopLoop(`pair:${target.fingerprint}:prev`, REVOKED)\n if (tombstone) startTombstoneLoops(tombstone)\n log(`[pairing] revoked ${target.fingerprint} (${target.name})`)\n return true\n }\n\n async function list(): Promise<PairingRecord[]> {\n await ensureLoaded()\n await refreshLocalDevices()\n return [\n ...Array.from(pairings.values()).map(r => ({ ...r })),\n ...Array.from(localDevices.values()).map(localDeviceView),\n ]\n }\n\n function findLocalDevice(idOrName: string): LocalDeviceRecord | undefined {\n const byFp = localDevices.get(idOrName)\n if (byFp) return byFp\n for (const rec of localDevices.values()) if (rec.name === idOrName) return rec\n return undefined\n }\n\n function localDeviceView(rec: LocalDeviceRecord): PairingRecord {\n return {\n clientPub: \"\",\n name: rec.name,\n fingerprint: rec.fingerprint,\n createdAt: rec.createdAt,\n lastSeen: rec.createdAt,\n pairRoot: \"\",\n rendezvousUrl: \"\",\n local: true,\n }\n }\n\n async function mintLocalDevice(input: { name: string }): Promise<LocalDeviceCredential> {\n const name = input.name.trim()\n if (!name) throw new Error(\"device name must not be empty\")\n await ensureLoaded()\n const rec = newLocalDevice(name, new Date(now()).toISOString())\n await mutateLocal(() => void localDevices.set(rec.fingerprint, rec))\n // The bearer is deliberately absent from this line.\n log(`[pairing] minted local device ${rec.fingerprint} (${rec.name})`)\n return {\n bearer: deriveLocalBearer(rec),\n fingerprint: rec.fingerprint,\n name: rec.name,\n createdAt: rec.createdAt,\n }\n }\n\n async function verifyDeviceBearer(\n bearer: string,\n ): Promise<{ fingerprint: string; name: string } | null> {\n await ensureLoaded()\n await refreshLocalDevices()\n const parsed = parseLocalBearer(bearer)\n const rec = parsed ? localDevices.get(parsed.fingerprint) : undefined\n return verifyLocalBearer(rec, parsed) && rec ? { fingerprint: rec.fingerprint, name: rec.name } : null\n }\n\n // Named `renamePairing`, not `rename` — this closure also calls the\n // `node:fs/promises` `rename` (in `persist()`'s atomic write) and a same-\n // named local function here would shadow that import for the rest of the\n // closure, silently breaking every persist. Exposed on the returned\n // registry as `rename`; only the internal binding avoids the collision.\n async function renamePairing(idOrName: string, newName: string): Promise<boolean> {\n const trimmed = newName.trim()\n if (!trimmed) throw new Error(\"device name must not be empty\")\n await ensureLoaded()\n let target: PairingRecord | undefined = pairings.get(idOrName)\n if (!target) {\n for (const rec of pairings.values()) {\n if (rec.name === idOrName) {\n target = rec\n break\n }\n }\n }\n if (!target) {\n await refreshLocalDevices()\n const device = findLocalDevice(idOrName)\n if (!device) return false\n await mutateLocal(() => {\n const current = localDevices.get(device.fingerprint)\n if (current) current.name = trimmed\n })\n log(`[pairing] renamed ${device.fingerprint} to \"${trimmed}\"`)\n return true\n }\n target.name = trimmed\n await persist()\n log(`[pairing] renamed ${target.fingerprint} to \"${trimmed}\"`)\n return true\n }\n\n function isOnline(fingerprint: string): boolean {\n return (onlineCounts.get(fingerprint) ?? 0) > 0\n }\n\n async function shutdown(): Promise<void> {\n if (shuttingDown) return\n shuttingDown = true\n for (const key of Array.from(loops.keys())) {\n await stopLoop(key)\n }\n for (const handle of Array.from(channels)) {\n await handle.close().catch(() => {})\n }\n channels.clear()\n }\n\n return {\n createOffer,\n list,\n rename: renamePairing,\n revoke,\n isOnline,\n startAutoconnect,\n shutdown,\n mintLocalDevice,\n verifyDeviceBearer,\n }\n}\n\n// ── helpers ────────────────────────────────────────────────────\n\nfunction dialUrl(rendezvousUrl: string, token: string): string {\n const sep = rendezvousUrl.includes(\"?\") ? \"&\" : \"?\"\n return `${rendezvousUrl}${sep}side=daemon&t=${encodeURIComponent(token)}`\n}\n\n/** Tell a pair/v1 peer to re-pair, over the notice-only channel, then close. */\nfunction sendOutdatedNotice(sink: E2eFrameSink): void {\n sink.send({ t: \"error\", code: \"pairing_protocol_outdated\", message: PAIRING_PROTOCOL_OUTDATED_MESSAGE })\n sink.send({\n t: \"hello\",\n // Deliberately not TUNNEL_VERSION: a v1 client reports an unknown version\n // verbatim, which is how the notice reaches its user (see\n // OUTDATED_HELLO_VERSION).\n version: OUTDATED_HELLO_VERSION as \"agentproto/tunnel/v1\",\n capabilities: { pty: false },\n label: \"pairing_protocol_outdated\",\n })\n // close() flushes already-queued frames before closing the transport.\n sink.close(\"pairing_protocol_outdated\")\n}\n\n/** Abort reason for a loop stopped by `revoke` — its live channel, if any, is\n * told `pairing_revoked` before it closes. */\nconst REVOKED = Symbol(\"pairing revoked\")\n\n/** Resolve when the sink closes (or `signal` aborts, closing it first) — with\n * the close reason the remote gave, when any, so the caller's log can say WHY\n * the channel came down. */\nfunction waitClosed(sink: E2eFrameSink, signal: AbortSignal): Promise<string | undefined> {\n return new Promise(resolve => {\n if (!sink.isOpen) {\n resolve(undefined)\n return\n }\n let settled = false\n const finish = (reason: string | undefined): void => {\n if (settled) return\n settled = true\n resolve(reason)\n }\n sink.onClose(reason => finish(reason))\n signal.addEventListener(\"abort\", () => {\n if (signal.reason === REVOKED) {\n sink.send(revokedFrame())\n sink.close(PAIRING_REVOKED_CODE)\n } else {\n sink.close(\"registry shutdown\")\n }\n finish(\"registry shutdown\")\n })\n })\n}\n\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise(resolve => {\n if (signal.aborted) return resolve()\n const timer = setTimeout(resolve, ms)\n if (typeof timer.unref === \"function\") timer.unref()\n signal.addEventListener(\"abort\", () => {\n clearTimeout(timer)\n resolve()\n })\n })\n}\n\nfunction isPairingsFile(v: unknown): v is PairingsFile {\n if (typeof v !== \"object\" || v === null) return false\n const rec = v as Record<string, unknown>\n if (rec[\"v\"] !== PAIRINGS_VERSION && rec[\"v\"] !== LEGACY_PAIRINGS_VERSION) return false\n if (!Array.isArray(rec[\"pairings\"])) return false\n return rec[\"pairings\"].every(isPairingRecord)\n}\n\nfunction isPairingRecord(v: unknown): v is PairingRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"clientPub\"] === \"string\" &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"fingerprint\"] === \"string\" &&\n typeof r[\"createdAt\"] === \"string\" &&\n typeof r[\"lastSeen\"] === \"string\" &&\n typeof r[\"pairRoot\"] === \"string\" &&\n typeof r[\"rendezvousUrl\"] === \"string\"\n )\n}\n\nfunction isRevokedRecord(v: unknown): v is RevokedPairingRecord {\n if (typeof v !== \"object\" || v === null) return false\n const r = v as Record<string, unknown>\n return (\n typeof r[\"fingerprint\"] === \"string\" &&\n typeof r[\"name\"] === \"string\" &&\n typeof r[\"rendezvousUrl\"] === \"string\" &&\n typeof r[\"revokedAt\"] === \"string\" &&\n typeof r[\"expiresAt\"] === \"string\" &&\n Array.isArray(r[\"routes\"]) &&\n r[\"routes\"].every(x => {\n if (typeof x !== \"object\" || x === null) return false\n const t = x as Record<string, unknown>\n return typeof t[\"epoch\"] === \"number\" && typeof t[\"route\"] === \"string\" && typeof t[\"auth\"] === \"string\"\n })\n )\n}\n\nfunction isEnoent(err: unknown): boolean {\n return (\n typeof err === \"object\" &&\n err !== null &&\n \"code\" in err &&\n (err as { code?: unknown }).code === \"ENOENT\"\n )\n}\n\nfunction errMsg(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n","import type { Agent } from \"node:http\"\nimport WebSocket from \"ws\"\nimport { wrapWebSocket, type FrameSink } from \"@agentproto/acp/tunnel\"\n\nexport interface DialRendezvousOptions {\n /** A `ws`-compatible agent (e.g. an HTTPS proxy agent). `ws` ignores\n * `HTTPS_PROXY` on its own, so a host behind a proxy passes one here. */\n agent?: Agent\n}\n\n/**\n * Dial a rendezvous broker outbound and adapt the socket to a `FrameSink`.\n * The default `PairingRegistryDeps.dial`. Rejects if the dial fails; honours\n * `signal` so a registry shutdown tears down an in-flight dial promptly.\n */\nexport async function dialRendezvous(\n url: string,\n signal: AbortSignal,\n opts: DialRendezvousOptions = {},\n): Promise<FrameSink> {\n const ws = new WebSocket(url, opts.agent ? { agent: opts.agent } : undefined)\n await new Promise<void>((resolve, reject) => {\n const cleanup = (): void => {\n ws.off(\"open\", onOpen)\n ws.off(\"error\", onError)\n signal.removeEventListener(\"abort\", onAbort)\n }\n const onOpen = (): void => {\n cleanup()\n resolve()\n }\n const onError = (err: Error): void => {\n cleanup()\n reject(err)\n }\n const onAbort = (): void => {\n cleanup()\n try {\n ws.close()\n } catch {\n /* ignore */\n }\n reject(new Error(\"dial aborted\"))\n }\n if (signal.aborted) {\n onAbort()\n return\n }\n ws.once(\"open\", onOpen)\n ws.once(\"error\", onError)\n signal.addEventListener(\"abort\", onAbort)\n })\n return wrapWebSocket(ws as unknown as Parameters<typeof wrapWebSocket>[0])\n}\n","import {\n createTunnelServer,\n DEFAULT_HTTP_FORWARD_TIMEOUT_MS,\n type E2eFrameSink,\n type TunnelFrame,\n} from \"@agentproto/acp/tunnel\"\nimport type { PairingChannelContext, PairingChannelHandle } from \"./pairing-registry.js\"\n\nexport interface ServeLoopbackHttpOptions {\n /** The local HTTP app the paired channel is forwarded to. Must be a\n * loopback http(s) URL (127.0.0.0/8, ::1 or localhost): a paired peer must\n * never be able to steer the host at another machine. */\n target: URL | string\n /** Headers set on EVERY forwarded request, overriding any same-named header\n * the peer sent (e.g. the host app's own bearer). */\n injectHeaders?: Readonly<Record<string, string>>\n /** Pathnames a peer may request. `\"/mcp\"` matches exactly, `\"/api/*\"`\n * matches `/api` and everything below it, a RegExp is tested against the\n * pathname. Omitted: any path. `[]`: nothing. A denied request gets a 403\n * `path_not_allowed` response and never reaches the target. */\n allowPaths?: readonly (string | RegExp)[]\n /** Label in the tunnel hello. Default \"loopback-http\". */\n label?: string\n httpForwardTimeoutMs?: number\n}\n\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"])\n\nfunction isLoopback(url: URL): boolean {\n return LOOPBACK_HOSTS.has(url.hostname) || /^127\\.\\d+\\.\\d+\\.\\d+$/.test(url.hostname)\n}\n\n/** Pathname of a frame path, or null when it must be refused outright (the\n * tunnel server already rejects `..` and non-rooted paths; encoded dot/slash\n * forms are refused here because the target may decode them). */\nfunction safePathname(path: string): string | null {\n if (!path.startsWith(\"/\") || path.startsWith(\"//\") || path.includes(\"\\\\\")) return null\n if (/%2e|%2f|%5c/i.test(path)) return null\n const end = path.search(/[?#]/)\n return end === -1 ? path : path.slice(0, end)\n}\n\nfunction pathAllowed(pathname: string, allow: readonly (string | RegExp)[]): boolean {\n return allow.some(rule => {\n if (rule instanceof RegExp) {\n rule.lastIndex = 0\n return rule.test(pathname)\n }\n if (rule.endsWith(\"/*\")) {\n const base = rule.slice(0, -2)\n return pathname === base || pathname.startsWith(`${base}/`)\n }\n return pathname === rule\n })\n}\n\n/**\n * Build a `PairingRegistryDeps.serve` that forwards the paired, E2E-wrapped\n * channel to a local HTTP app: `http_request` frames become fetches against\n * `target` (with `injectHeaders` applied), responses stream back. Spawn, PTY\n * and WebSocket frames are not served.\n */\nexport function serveLoopbackHttp(\n opts: ServeLoopbackHttpOptions,\n): (sink: E2eFrameSink, ctx: PairingChannelContext) => PairingChannelHandle {\n const target = typeof opts.target === \"string\" ? new URL(opts.target) : opts.target\n if (target.protocol !== \"http:\" && target.protocol !== \"https:\") {\n throw new Error(`serveLoopbackHttp: target must be http(s), got ${target.protocol}`)\n }\n if (!isLoopback(target)) {\n throw new Error(`serveLoopbackHttp: target must be a loopback address, got ${target.hostname}`)\n }\n const httpUpstream = target.origin + target.pathname.replace(/\\/$/, \"\")\n const allow = opts.allowPaths\n\n return sink => {\n const guarded = allow ? guardSink(sink, allow) : sink\n const server = createTunnelServer({\n sink: guarded,\n label: opts.label ?? \"loopback-http\",\n pty: false,\n authorize: () => null,\n httpUpstream,\n ...(opts.injectHeaders && Object.keys(opts.injectHeaders).length > 0\n ? { httpInjectHeaders: opts.injectHeaders }\n : {}),\n httpForwardTimeoutMs: opts.httpForwardTimeoutMs ?? DEFAULT_HTTP_FORWARD_TIMEOUT_MS,\n httpStreamIdleTimeoutMs: 120_000,\n })\n return { close: () => server.close() }\n }\n}\n\n/** A view of `sink` whose inbound `http_request` frames outside `allow` are\n * answered 403 here instead of being delivered to the tunnel server. */\nfunction guardSink(sink: E2eFrameSink, allow: readonly (string | RegExp)[]): E2eFrameSink {\n const denied = new Set<string>()\n const deny = (reqId: string, path: string): void => {\n denied.add(reqId)\n sink.send({\n t: \"http_response\",\n reqId,\n status: 403,\n error: { code: \"path_not_allowed\", message: `path '${path}' is not allowed on this host` },\n })\n }\n return {\n send: frame => sink.send(frame),\n close: reason => sink.close(reason),\n onClose: handler => sink.onClose(handler),\n get isOpen() {\n return sink.isOpen\n },\n get sentCount() {\n return sink.sentCount\n },\n get recvCount() {\n return sink.recvCount\n },\n onFrame: handler =>\n sink.onFrame((frame: TunnelFrame) => {\n if (frame.t === \"http_request\") {\n const pathname = safePathname(frame.path)\n if (pathname === null || !pathAllowed(pathname, allow)) {\n deny(frame.reqId, pathname ?? \"(invalid)\")\n return\n }\n } else if (frame.t === \"http_request_chunk\" && denied.has(frame.reqId)) {\n if (frame.end) denied.delete(frame.reqId)\n return\n }\n handler(frame)\n }),\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/pairing-host",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "@agentproto/pairing-host — the host side of AIP-59 pairing as a library: offer store, pair/v2 handshake over an untrusted rendezvous, persisted pairings with reconnect and revocation, a default ws rendezvous dialer, a loopback HTTP forwarder for the paired channel, and an optional local-device bearer credential. Lets a non-daemon host embed pairing without depending on @agentproto/runtime.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -43,7 +43,7 @@
43
43
  "sideEffects": false,
44
44
  "dependencies": {
45
45
  "ws": "^8.21.3",
46
- "@agentproto/acp": "0.9.1",
46
+ "@agentproto/acp": "0.10.0",
47
47
  "@agentproto/secrets": "1.2.1"
48
48
  },
49
49
  "devDependencies": {