import { EnvironmentRenderBootstrapPairingResult } from "@t3tools/contracts";import * as Option from "effect/Option";import * as Schema from "effect/Schema"; const RenderBootstrapErrorBody = Schema.Struct({ message: Schema.String });const decodeRenderBootstrapErrorBody = Schema.decodeUnknownOption(RenderBootstrapErrorBody);const decodeRenderBootstrapPairingResult = Schema.decodeUnknownSync( EnvironmentRenderBootstrapPairingResult,); export class RenderBootstrapRequestError extends Error { readonly status: number | null; constructor(message: string, status: number | null = null) { super(message); this.name = "RenderBootstrapRequestError"; this.status = status; }} export function normalizeRenderServiceUrl(value: string): string { const candidate = value.trim(); if (candidate.length === 0) { throw new RenderBootstrapRequestError("Enter the Render service URL."); } let url: URL; try { url = new URL(/^https?:\/\//iu.test(candidate) ? candidate : `https://${candidate}`); } catch { throw new RenderBootstrapRequestError("Enter a valid Render service URL."); } if (url.protocol !== "https:") { throw new RenderBootstrapRequestError("Render service URLs must use HTTPS."); } if (url.hostname.toLowerCase() === "dashboard.render.com") { throw new RenderBootstrapRequestError( "Paste the service's .onrender.com URL, not the Render dashboard URL.", ); } return url.origin;} export function maskedRenderServiceUrl(value: string): string { try { const hostname = new URL(value).hostname; if (hostname.length <= 28) return hostname; return `${hostname.slice(0, 13)}…${hostname.slice(-14)}`; } catch { return "Render service"; }} export async function requestRenderPairing(input: { readonly serviceUrl: string; readonly fetcher?: typeof globalThis.fetch;}): Promise<{ readonly host: string; readonly pairingCode: string; readonly label: string }> { const host = normalizeRenderServiceUrl(input.serviceUrl); const endpoint = new URL("/.well-known/t3/render/pair", host); let response: Response; try { response = await (input.fetcher ?? globalThis.fetch)(endpoint, { method: "POST", cache: "no-store", referrerPolicy: "no-referrer", }); } catch { throw new RenderBootstrapRequestError( "Could not reach the Render service. A free instance can take about a minute to wake up.", ); } const responseBody: unknown = await response.json().catch(() => null); if (!response.ok) { const decodedError = decodeRenderBootstrapErrorBody(responseBody); const serverMessage = Option.isSome(decodedError) ? decodedError.value.message : null; const message = response.status === 403 || response.status === 404 ? "This service does not have in-app Render pairing yet. Deploy the latest version." : serverMessage || `Render pairing failed with HTTP ${response.status}.`; throw new RenderBootstrapRequestError(message, response.status); } try { const pairing = decodeRenderBootstrapPairingResult(responseBody); return { host, pairingCode: pairing.pairingCode, label: pairing.label }; } catch { throw new RenderBootstrapRequestError("The Render service returned an invalid pairing reply."); }}
import { EnvironmentRenderBootstrapPairingResult } from "@t3tools/contracts";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
const RenderBootstrapErrorBody = Schema.Struct({ message: Schema.String });
const decodeRenderBootstrapErrorBody = Schema.decodeUnknownOption(RenderBootstrapErrorBody);
const decodeRenderBootstrapPairingResult = Schema.decodeUnknownSync(
EnvironmentRenderBootstrapPairingResult,
);
export class RenderBootstrapRequestError extends Error {
readonly status: number | null;
constructor(message: string, status: number | null = null) {
super(message);
this.name = "RenderBootstrapRequestError";
this.status = status;
}
}
export function normalizeRenderServiceUrl(value: string): string {
const candidate = value.trim();
if (candidate.length === 0) {
throw new RenderBootstrapRequestError("Enter the Render service URL.");
}
let url: URL;
try {
url = new URL(/^https?:\/\//iu.test(candidate) ? candidate : `https://${candidate}`);
} catch {
throw new RenderBootstrapRequestError("Enter a valid Render service URL.");
}
if (url.protocol !== "https:") {
throw new RenderBootstrapRequestError("Render service URLs must use HTTPS.");
}
if (url.hostname.toLowerCase() === "dashboard.render.com") {
throw new RenderBootstrapRequestError(
"Paste the service's .onrender.com URL, not the Render dashboard URL.",
);
}
return url.origin;
}
export function maskedRenderServiceUrl(value: string): string {
try {
const hostname = new URL(value).hostname;
if (hostname.length <= 28) return hostname;
return `${hostname.slice(0, 13)}…${hostname.slice(-14)}`;
} catch {
return "Render service";
}
}
export async function requestRenderPairing(input: {
readonly serviceUrl: string;
readonly fetcher?: typeof globalThis.fetch;
}): Promise<{ readonly host: string; readonly pairingCode: string; readonly label: string }> {
const host = normalizeRenderServiceUrl(input.serviceUrl);
const endpoint = new URL("/.well-known/t3/render/pair", host);
let response: Response;
try {
response = await (input.fetcher ?? globalThis.fetch)(endpoint, {
method: "POST",
cache: "no-store",
referrerPolicy: "no-referrer",
});
} catch {
throw new RenderBootstrapRequestError(
"Could not reach the Render service. A free instance can take about a minute to wake up.",
);
}
const responseBody: unknown = await response.json().catch(() => null);
if (!response.ok) {
const decodedError = decodeRenderBootstrapErrorBody(responseBody);
const serverMessage = Option.isSome(decodedError) ? decodedError.value.message : null;
const message =
response.status === 403 || response.status === 404
? "This service does not have in-app Render pairing yet. Deploy the latest version."
: serverMessage || `Render pairing failed with HTTP ${response.status}.`;
throw new RenderBootstrapRequestError(message, response.status);
}
try {
const pairing = decodeRenderBootstrapPairingResult(responseBody);
return { host, pairingCode: pairing.pairingCode, label: pairing.label };
} catch {
throw new RenderBootstrapRequestError("The Render service returned an invalid pairing reply.");
}
}