/** * Relay protocol for cross-machine presence (ADR 0016, phase 2): the shared, * host-agnostic logic both relay hosts (the Cloudflare Durable Objects worker * and `harn relay serve`) and the client transport build on. * * Capability-based rooms: the room id and the E2E symmetric key are BOTH * derived client-side via HKDF-SHA256 from (repo root-commit SHA, normalized * origin URL, a rotatable salt). Anyone who can read the repo can derive them; * nobody else can — including the relay operator, who sees only an opaque * room id, opaque sender ids, and AES-GCM ciphertext. Rotating the salt * rotates the room. * * The committed salt is NOT a secret and must never be treated as one: the * derivation needs the root-commit SHA + origin URL too, which only * repo-readers have. Repo access is the trust boundary (same as the git-refs * transport). * * Wire format (JSON text frames over WebSocket): * client → relay: { t: "pub", sender, iv, ct } broadcast + cache * relay → client: { t: "pub", sender, iv, ct } live or warm-join replay * relay → client: { t: "hello", peers } on join (peer count) * The relay never sees plaintext: `ct` is AES-GCM over the presence-blob JSON, * `sender` is an HMAC-derived opaque id, `iv` is the per-message nonce. * * Uses WebCrypto only (globalThis.crypto.subtle) so the same module runs on * Bun, Node ≥ 20, and Cloudflare Workers. */ /** Default derivation salt when a repo hasn't minted `.harnery/presence-salt`. * Public by design (see module docs); committing a random per-repo salt just * rotates the room away from anything derived before. */ export declare const DEFAULT_ROOM_SALT = "harnery-presence-salt/v1"; export interface RoomInputs { /** SHA of the repo's root commit (`git rev-list --max-parents=0 HEAD`, * first line). Stable for the repo's whole life. */ rootCommitSha: string; /** The origin remote URL, any format — normalized internally so ssh and * https clones of the same repo land in the same room. */ originUrl: string; /** Rotatable room salt (default DEFAULT_ROOM_SALT). */ salt?: string; } export interface RoomCredentials { /** Opaque 32-hex-char room id — the only room identity the relay sees. */ roomId: string; /** AES-GCM-256 key for payload E2E encryption. Never leaves the client. */ key: CryptoKey; /** Raw bytes for deriving per-machine opaque sender ids. */ senderKeyBytes: Uint8Array; } /** * Normalize a git remote URL so every clone shape of the same repo derives * the same room: `git@github.com:Org/Repo.git`, `ssh://git@github.com/org/repo`, * and `https://github.com/org/repo.git` all → `github.com/org/repo`. */ export declare function normalizeOriginUrl(raw: string): string; /** Derive the room id, E2E key, and sender-id key from the repo identity. */ export declare function deriveRoomCredentials(inputs: RoomInputs): Promise; /** Opaque, stable per-machine sender id: HMAC(senderKey, machineLabel). The * relay caches last-message-per-sender by this without learning the label. */ export declare function computeSenderId(creds: RoomCredentials, machineLabel: string): Promise; /** AES-GCM-encrypt a plaintext payload → { iv, ct } base64 pair. */ export declare function encryptPayload(creds: RoomCredentials, plaintext: string): Promise<{ iv: string; ct: string; }>; /** Decrypt an { iv, ct } pair. Returns null on any failure (wrong room key, * tampered frame) — callers drop the frame silently. */ export declare function decryptPayload(creds: RoomCredentials, frame: { iv: string; ct: string; }): Promise; /** Client → relay and relay → client publication frame. */ export interface PubFrame { t: "pub"; /** Opaque sender id (computeSenderId). The relay keys its warm-join cache on this. */ sender: string; iv: string; ct: string; } /** Relay → client greeting on join. */ export interface HelloFrame { t: "hello"; /** Live sockets in the room at join time (including the joiner). */ peers: number; } export type RelayFrame = PubFrame | HelloFrame; /** Hard cap a relay enforces per text frame (ciphertext of a ~2KB blob is far * smaller; anything bigger is abuse or a bug). */ export declare const MAX_FRAME_BYTES: number; /** Parse + shape-validate an incoming relay frame. Null on anything off. */ export declare function parseRelayFrame(raw: string): RelayFrame | null; /** Room ids are 32 lowercase hex chars; relays reject anything else. */ export declare function isValidRoomId(roomId: string): boolean; //# sourceMappingURL=relay-protocol.d.ts.map