import type { AdvertisedEndpoint, AdvertisedEndpointHostedHttpsCompatibility, AdvertisedEndpointProvider, AdvertisedEndpointReachability, AdvertisedEndpointSource, AdvertisedEndpointStatus, } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { getUrlDiagnostics } from "./urlDiagnostics.ts"; const AdvertisedEndpointInputDiagnosticFields = { inputLength: Schema.Number, inputProtocol: Schema.optional(Schema.String), inputHostname: Schema.optional(Schema.String), }; function advertisedEndpointInputDiagnostics(input: string) { const diagnostics = getUrlDiagnostics(input); return { inputLength: diagnostics.inputLength, ...(diagnostics.protocol === undefined ? {} : { inputProtocol: diagnostics.protocol }), ...(diagnostics.hostname === undefined ? {} : { inputHostname: diagnostics.hostname }), }; } export class AdvertisedEndpointUrlParseError extends Schema.TaggedErrorClass()( "AdvertisedEndpointUrlParseError", { ...AdvertisedEndpointInputDiagnosticFields, cause: Schema.Defect(), }, ) { override get message(): string { return "Invalid advertised endpoint URL."; } } export class AdvertisedEndpointProtocolError extends Schema.TaggedErrorClass()( "AdvertisedEndpointProtocolError", { ...AdvertisedEndpointInputDiagnosticFields, protocol: Schema.String, }, ) { override get message(): string { return `Endpoint must use HTTP or HTTPS. Received ${this.protocol}`; } } export interface CreateAdvertisedEndpointInput { readonly id: string; readonly label: string; readonly provider: AdvertisedEndpointProvider; readonly httpBaseUrl: string; readonly reachability: AdvertisedEndpointReachability; readonly hostedHttpsCompatibility?: AdvertisedEndpointHostedHttpsCompatibility; readonly desktopCompatibility?: "compatible" | "unknown"; readonly source: AdvertisedEndpointSource; readonly status?: AdvertisedEndpointStatus; readonly isDefault?: boolean; readonly description?: string; } export function normalizeHttpBaseUrl(rawValue: string): string { let url: URL; try { url = new URL(rawValue); } catch (cause) { throw new AdvertisedEndpointUrlParseError({ ...advertisedEndpointInputDiagnostics(rawValue), cause, }); } if (url.protocol === "ws:") { url.protocol = "http:"; } else if (url.protocol === "wss:") { url.protocol = "https:"; } if (url.protocol !== "http:" && url.protocol !== "https:") { throw new AdvertisedEndpointProtocolError({ ...advertisedEndpointInputDiagnostics(rawValue), protocol: url.protocol, }); } url.pathname = "/"; url.search = ""; url.hash = ""; return url.toString(); } export function deriveWsBaseUrl(httpBaseUrl: string): string { const url = new URL(normalizeHttpBaseUrl(httpBaseUrl)); url.protocol = url.protocol === "https:" ? "wss:" : "ws:"; return url.toString(); } export function classifyHostedHttpsCompatibility( httpBaseUrl: string, fallback: AdvertisedEndpointHostedHttpsCompatibility = "unknown", ): AdvertisedEndpointHostedHttpsCompatibility { const url = new URL(normalizeHttpBaseUrl(httpBaseUrl)); if (url.protocol === "http:") { return "mixed-content-blocked"; } return fallback === "mixed-content-blocked" ? "unknown" : fallback; } export function createAdvertisedEndpoint(input: CreateAdvertisedEndpointInput): AdvertisedEndpoint { const httpBaseUrl = normalizeHttpBaseUrl(input.httpBaseUrl); return { id: input.id, label: input.label, provider: input.provider, httpBaseUrl, wsBaseUrl: deriveWsBaseUrl(httpBaseUrl), reachability: input.reachability, compatibility: { hostedHttpsApp: input.hostedHttpsCompatibility ?? classifyHostedHttpsCompatibility(httpBaseUrl), desktopApp: input.desktopCompatibility ?? "compatible", }, source: input.source, status: input.status ?? "available", ...(input.isDefault === undefined ? {} : { isDefault: input.isDefault }), ...(input.description === undefined ? {} : { description: input.description }), }; }