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>()( "AdvertisedEndpointUrlParseError", { ...AdvertisedEndpointInputDiagnosticFields, cause: Schema.Defect(), },) { override get message(): string { return "Invalid advertised endpoint URL."; }} export class AdvertisedEndpointProtocolError extends Schema.TaggedErrorClass<AdvertisedEndpointProtocolError>()( "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 }), };}
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>()(
"AdvertisedEndpointUrlParseError",
{
...AdvertisedEndpointInputDiagnosticFields,
cause: Schema.Defect(),
},
) {
override get message(): string {
return "Invalid advertised endpoint URL.";
}
}
export class AdvertisedEndpointProtocolError extends Schema.TaggedErrorClass<AdvertisedEndpointProtocolError>()(
"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 }),
};
}