81 lines
2.2 KiB
TypeScript
81 lines
2.2 KiB
TypeScript
/**
|
|
* Two-device handoff payload.
|
|
*
|
|
* The NEW device publishes a key package to the coordinator and shows this
|
|
* compact code as a QR. An EXISTING member scans it, consumes the key package
|
|
* by ref, and the coordinator stores the Welcome for the new device. The
|
|
* private key never leaves the new device, and the code is short enough for a
|
|
* normal QR.
|
|
*/
|
|
export interface CordnDeviceLink {
|
|
gid: string;
|
|
kpRef: string;
|
|
pubkey: string;
|
|
coordinatorPubkey: string;
|
|
relays: string[];
|
|
}
|
|
|
|
const PREFIX = 'cordn-device:';
|
|
const HEX64 = /^[0-9a-f]{64}$/;
|
|
|
|
function b64urlEncode(value: string): string {
|
|
return btoa(value).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
}
|
|
|
|
function b64urlDecode(value: string): string {
|
|
const padded = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
const pad = padded.length % 4 === 0 ? '' : '='.repeat(4 - (padded.length % 4));
|
|
return atob(padded + pad);
|
|
}
|
|
|
|
export function encodeDeviceLink(link: CordnDeviceLink): string {
|
|
const json = JSON.stringify({
|
|
g: link.gid,
|
|
k: link.kpRef,
|
|
p: link.pubkey,
|
|
c: link.coordinatorPubkey,
|
|
r: link.relays,
|
|
});
|
|
return `${PREFIX}${b64urlEncode(json)}`;
|
|
}
|
|
|
|
export function decodeDeviceLink(raw: string | undefined | null): CordnDeviceLink | null {
|
|
if (!raw || !raw.startsWith(PREFIX)) return null;
|
|
try {
|
|
const parsed = JSON.parse(b64urlDecode(raw.slice(PREFIX.length))) as {
|
|
g?: unknown;
|
|
k?: unknown;
|
|
p?: unknown;
|
|
c?: unknown;
|
|
r?: unknown;
|
|
};
|
|
if (
|
|
typeof parsed.g !== 'string' ||
|
|
!parsed.g ||
|
|
typeof parsed.k !== 'string' ||
|
|
!parsed.k ||
|
|
typeof parsed.p !== 'string' ||
|
|
!HEX64.test(parsed.p) ||
|
|
typeof parsed.c !== 'string' ||
|
|
!HEX64.test(parsed.c) ||
|
|
!Array.isArray(parsed.r) ||
|
|
parsed.r.length === 0
|
|
) {
|
|
return null;
|
|
}
|
|
const relays = parsed.r.filter(
|
|
(relay): relay is string =>
|
|
typeof relay === 'string' && /^wss?:\/\/[^\s]+$/i.test(relay),
|
|
);
|
|
if (relays.length === 0) return null;
|
|
return {
|
|
gid: parsed.g,
|
|
kpRef: parsed.k,
|
|
pubkey: parsed.p,
|
|
coordinatorPubkey: parsed.c,
|
|
relays,
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|