12345import { computeVaultId } from "../shared/sync-identity";6import { grantAdoption, isAdoptRequest, type AdoptRequest } from "../shared/chirp-adopt";7import type { LocalIdentity, PairingHandshakeResult, PairingTransport } from "../shared/chirp-handshake";8import type { PairedDevice } from "../shared/sync-types";910interface SourceAdoptionDeps {11 confirmSAS(sas: string): Promise<boolean>;12 completePairing(device: PairedDevice): Promise<void>;13 grantConsent(request: AdoptRequest, sas: string): Promise<boolean>;14 fetchRootKeyHex(): Promise<string>;15 fetchLicense?(): Promise<string | undefined>;16 deriveVaultIdentity(): Promise<LocalIdentity>;17 upsertPairedDevice(device: PairedDevice): Promise<void>;18}1920interface PostNoiseFrame {21 sas: string;22 frame: unknown;23 channel: PairingTransport;24}25262728293031declare function runPairingResponderOpenFrame(32 ourEphemeralPrivateKey: Uint8Array,33 ourEphemeralPublicKey: Uint8Array,34 ourIdentity: LocalIdentity,35 transport: PairingTransport,36): Promise<PostNoiseFrame>;3738declare function handlePairIdentityFrame(39 frame: unknown,40 channel: PairingTransport,41 localIdentity: LocalIdentity,42): Promise<PairingHandshakeResult>;4344export async function routeSourcePostSASFrame(45 postNoise: PostNoiseFrame,46 localIdentity: LocalIdentity,47 deps: SourceAdoptionDeps,48): Promise<PairedDevice | null> {49 const sasConfirmed = await deps.confirmSAS(postNoise.sas);50 if (!sasConfirmed) {51 await closeIfPossible(postNoise.channel);52 throw new Error("sas_rejected");53 }5455 if (isAdoptRequest(postNoise.frame)) {56 const rootKeyHex = await deps.fetchRootKeyHex();57 if (!/^[0-9a-f]{64}$/.test(rootKeyHex)) throw new Error("ADOPT_INVALID_ROOT_KEY");58 const rootKeyBytes = hexToBytes(rootKeyHex);59 try {60 const vaultId = await computeVaultId(rootKeyBytes);61 await grantAdoption(postNoise.channel, postNoise.frame, {62 sourceIdentity: localIdentity,63 rootKeyHex: async () => rootKeyHex,64 vaultId: async () => vaultId,65 license: deps.fetchLicense,66 onConsent: (request) => deps.grantConsent(request, postNoise.sas),67 upsertPairedDevice: deps.upsertPairedDevice,68 });69 return null;70 } finally {71 rootKeyBytes.fill(0);72 73 74 }75 }7677 if (isPairIdentityFrame(postNoise.frame)) {78 const result = await handlePairIdentityFrame(postNoise.frame, postNoise.channel, localIdentity);79 await deps.completePairing(result.device);80 return result.device;81 }8283 await closeIfPossible(postNoise.channel);84 throw new Error("unexpected_pairing_frame");85}8687function isPairIdentityFrame(frame: unknown): boolean {88 return (frame as { type?: unknown } | null)?.type === "PAIR_IDENTITY";89}9091async function closeIfPossible(channel: PairingTransport): Promise<void> {92 const maybe = channel as PairingTransport & { close?: () => Promise<void> };93 await maybe.close?.();94}9596function hexToBytes(hex: string): Uint8Array {97 const out = new Uint8Array(hex.length / 2);98 for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);99 return out;100}101