1import { readHostedPairingRequest } from "@t3tools/shared/remote";
2import * as Schema from "effect/Schema";
3
4const MOBILE_PAIRING_URL_PARAM = "pairingUrl";
5
6function isIpLiteral(host: string): boolean {
7 try {
8 const hostname = new URL(`http://${host}`).hostname.replace(/^\[|\]$/g, "");
9 if (hostname.includes(":")) return true;
10
11 const octets = hostname.split(".");
12 return (
13 octets.length === 4 &&
14 octets.every((octet) => /^\d{1,3}$/.test(octet) && Number(octet) <= 255)
15 );
16 } catch {
17 return false;
18 }
19}
20
21export class PairingQrPayloadEmptyError extends Schema.TaggedError<PairingQrPayloadEmptyError>()(
22 "PairingQrPayloadEmptyError",
23 {},
24) {
25 override get message(): string {
26 return "Scanned QR code did not contain a pairing URL.";
27 }
28}
29
30export function buildPairingUrl(host: string, code: string): string {
31 const h = host.trim();
32 const c = code.trim();
33 if (!h) return "";
34 if (!c) return h;
35
36 try {
37 const url = new URL(h.includes("://") ? h : `${isIpLiteral(h) ? "http" : "https"}://${h}`);
38 url.hash = new URLSearchParams([["token", c]]).toString();
39 return url.toString();
40 } catch {
41 return `${h}#token=${c}`;
42 }
43}
44
45export function parsePairingUrl(url: string): { host: string; code: string } {
46 const trimmed = url.trim();
47 if (!trimmed) return { host: "", code: "" };
48
49 try {
50 const parsed = new URL(trimmed);
51 const hostedPairingRequest = readHostedPairingRequest(parsed);
52 if (hostedPairingRequest) {
53 return {
54 host: hostedPairingRequest.host.replace(/\/$/, ""),
55 code: hostedPairingRequest.token,
56 };
57 }
58
59 const hashParams = new URLSearchParams(parsed.hash.slice(1));
60 const hashToken = hashParams.get("token");
61 const queryToken = parsed.searchParams.get("token");
62 const code = hashToken || queryToken || "";
63
64 parsed.hash = "";
65 parsed.search = "";
66 parsed.pathname = parsed.pathname.replace(/\/pair\/?$/, "/");
67 return { host: parsed.toString().replace(/\/$/, ""), code };
68 } catch {
69 return { host: trimmed, code: "" };
70 }
71}
72
73export function extractPairingUrlFromQrPayload(payload: string): string {
74 const trimmed = payload.trim();
75 if (!trimmed) {
76 throw new PairingQrPayloadEmptyError({});
77 }
78
79 try {
80 const url = new URL(trimmed);
81 if (url.protocol === "t3code:") {
82 const pairingUrl = url.searchParams.get(MOBILE_PAIRING_URL_PARAM)?.trim() ?? "";
83 if (pairingUrl.length > 0) {
84 return pairingUrl;
85 }
86 }
87 } catch {
88 // Treat non-URL payloads as raw pairing-url text so the normal input validation can decide.
89 }
90
91 return trimmed;
92}