import * as Schema from "effect/Schema"; const PAIRING_TOKEN_PARAM = "token"; const HOSTED_PAIRING_HOST_PARAM = "host"; const HOSTED_PAIRING_LABEL_PARAM = "label"; const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]); export const readHashParams = (url: URL): URLSearchParams => new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash); export class RemoteBackendUrlMissingError extends Schema.TaggedError()( "RemoteBackendUrlMissingError", {}, ) { override get message(): string { return "Enter a backend URL."; } } export class RemotePairingUrlInvalidError extends Schema.TaggedError()( "RemotePairingUrlInvalidError", { cause: Schema.optional(Schema.Defect()), protocol: Schema.optional(Schema.String), }, ) { override get message(): string { return "Pairing URL is invalid."; } } export class RemoteBackendUrlInvalidError extends Schema.TaggedError()( "RemoteBackendUrlInvalidError", { source: Schema.Literals(["direct-host", "hosted-pairing-host"]), cause: Schema.optional(Schema.Defect()), protocol: Schema.optional(Schema.String), }, ) { override get message(): string { return "Backend URL is invalid."; } } export class RemotePairingTokenMissingError extends Schema.TaggedError()( "RemotePairingTokenMissingError", { host: Schema.String }, ) { override get message(): string { return "Pairing URL is missing its token."; } } export class RemotePairingCodeMissingError extends Schema.TaggedError()( "RemotePairingCodeMissingError", { host: Schema.String }, ) { override get message(): string { return "Enter a pairing code."; } } export const RemotePairingTargetError = Schema.Union([ RemoteBackendUrlMissingError, RemotePairingUrlInvalidError, RemoteBackendUrlInvalidError, RemotePairingTokenMissingError, RemotePairingCodeMissingError, ]); export type RemotePairingTargetError = typeof RemotePairingTargetError.Type; const hasSupportedRemoteBackendProtocol = (url: URL): boolean => SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol); const normalizeRemoteBaseUrl = ( rawValue: string, source: RemoteBackendUrlInvalidError["source"], ): URL => { const trimmed = rawValue.trim(); if (!trimmed) { throw new RemoteBackendUrlMissingError(); } const withoutLeadingSlashes = trimmed.replace(/^\/+/, ""); const normalizedInput = /^[a-zA-Z][a-zA-Z\d+-]*:\/\//.test(withoutLeadingSlashes) ? withoutLeadingSlashes : `https://${withoutLeadingSlashes}`; let url: URL; try { url = new URL(normalizedInput); } catch (cause) { throw new RemoteBackendUrlInvalidError({ source, cause }); } if (!hasSupportedRemoteBackendProtocol(url)) { throw new RemoteBackendUrlInvalidError({ source, protocol: url.protocol, }); } url.pathname = "/"; url.search = ""; url.hash = ""; return url; }; const toHttpBaseUrl = (url: URL): string => { const next = new URL(url.toString()); if (next.protocol === "ws:") { next.protocol = "http:"; } else if (next.protocol === "wss:") { next.protocol = "https:"; } next.pathname = "/"; next.search = ""; next.hash = ""; return next.toString(); }; const toWsBaseUrl = (url: URL): string => { const next = new URL(url.toString()); if (next.protocol === "http:") { next.protocol = "ws:"; } else if (next.protocol === "https:") { next.protocol = "wss:"; } next.pathname = "/"; next.search = ""; next.hash = ""; return next.toString(); }; export interface ResolvedRemotePairingTarget { readonly credential: string; readonly httpBaseUrl: string; readonly wsBaseUrl: string; } export interface HostedPairingRequest { readonly host: string; readonly token: string; readonly label: string; } export const getPairingTokenFromUrl = (url: URL): string | null => { const hashToken = readHashParams(url).get(PAIRING_TOKEN_PARAM)?.trim() ?? ""; if (hashToken.length > 0) { return hashToken; } const searchToken = url.searchParams.get(PAIRING_TOKEN_PARAM)?.trim() ?? ""; return searchToken.length > 0 ? searchToken : null; }; export const stripPairingTokenFromUrl = (url: URL): URL => { const next = new URL(url.toString()); const hashParams = readHashParams(next); if (hashParams.has(PAIRING_TOKEN_PARAM)) { hashParams.delete(PAIRING_TOKEN_PARAM); next.hash = hashParams.toString(); } next.searchParams.delete(PAIRING_TOKEN_PARAM); return next; }; export const setPairingTokenOnUrl = (url: URL, credential: string): URL => { const next = new URL(url.toString()); next.searchParams.delete(PAIRING_TOKEN_PARAM); next.hash = new URLSearchParams([[PAIRING_TOKEN_PARAM, credential]]).toString(); return next; }; export const readHostedPairingRequest = (url: URL): HostedPairingRequest | null => { const host = url.searchParams.get(HOSTED_PAIRING_HOST_PARAM)?.trim() ?? ""; const token = getPairingTokenFromUrl(url)?.trim() ?? ""; const label = url.searchParams.get(HOSTED_PAIRING_LABEL_PARAM)?.trim() ?? ""; if (!host || !token) { return null; } return { host, token, label, }; }; export const resolveRemotePairingTarget = (input: { readonly pairingUrl?: string; readonly host?: string; readonly pairingCode?: string; }): ResolvedRemotePairingTarget => { const pairingUrl = input.pairingUrl?.trim() ?? ""; if (pairingUrl.length > 0) { let url: URL; try { url = new URL(pairingUrl); } catch (cause) { throw new RemotePairingUrlInvalidError({ cause }); } if (!hasSupportedRemoteBackendProtocol(url)) { throw new RemotePairingUrlInvalidError({ protocol: url.protocol, }); } const hostedPairingRequest = readHostedPairingRequest(url); if (hostedPairingRequest) { const hostedBackendUrl = normalizeRemoteBaseUrl( hostedPairingRequest.host, "hosted-pairing-host", ); return { credential: hostedPairingRequest.token, httpBaseUrl: toHttpBaseUrl(hostedBackendUrl), wsBaseUrl: toWsBaseUrl(hostedBackendUrl), }; } const credential = getPairingTokenFromUrl(url) ?? ""; if (!credential) { throw new RemotePairingTokenMissingError({ host: url.host }); } return { credential, httpBaseUrl: toHttpBaseUrl(url), wsBaseUrl: toWsBaseUrl(url), }; } const host = input.host?.trim() ?? ""; const pairingCode = input.pairingCode?.trim() ?? ""; if (!host) { throw new RemoteBackendUrlMissingError(); } const normalizedHost = normalizeRemoteBaseUrl(host, "direct-host"); if (!pairingCode) { throw new RemotePairingCodeMissingError({ host: normalizedHost.host }); } return { credential: pairingCode, httpBaseUrl: toHttpBaseUrl(normalizedHost), wsBaseUrl: toWsBaseUrl(normalizedHost), }; };