export class LocalUiHandoffError extends Error {
    code;
    statusCode;
    constructor(code, message, statusCode) {
        super(message);
        this.code = code;
        this.statusCode = statusCode;
        this.name = "LocalUiHandoffError";
    }
}
export async function mintLocalUiHandoff(input) {
    const normalized = normalizeInput(input);
    const fetchImpl = input.fetch ?? globalThis.fetch;
    if (!fetchImpl) {
        throw new LocalUiHandoffError("invalid_request", "No fetch implementation is available in this Node runtime.");
    }
    let response;
    try {
        response = await fetchImpl(new URL("/local-ui/capabilities", normalized.bridgeUrl), {
            method: "POST",
            headers: {
                "content-type": "application/json",
                "x-patchtrail-bridge-token": normalized.bridgeToken,
            },
            body: JSON.stringify({
                origin: normalized.origin,
                workspaceId: normalized.workspaceId,
                deviceId: normalized.deviceId,
                userSessionId: normalized.userSessionId,
                scopes: normalized.scopes,
                ...(normalized.ttlSeconds
                    ? { ttlSeconds: normalized.ttlSeconds }
                    : {}),
                ...(normalized.sessionTtlSeconds
                    ? { sessionTtlSeconds: normalized.sessionTtlSeconds }
                    : {}),
            }),
        });
    }
    catch (error) {
        throw new LocalUiHandoffError("network_error", `Unable to reach local PatchTrail bridge: ${error instanceof Error ? error.message : "unknown error"}`);
    }
    const payload = await readJsonObject(response);
    if (!response.ok) {
        throw new LocalUiHandoffError("bridge_rejected", readErrorMessage(payload), response.status);
    }
    const capabilityToken = readString(payload.token, "token");
    const expiresAt = readString(payload.expiresAt, "expiresAt");
    const capabilityId = readString(payload.capabilityId, "capabilityId");
    const handoffUrl = buildLocalUiHandoffUrl({
        uiUrl: normalized.uiUrl,
        payload: {
            version: 1,
            bridgeUrl: normalized.bridgeUrl,
            capabilityToken,
            workspaceId: normalized.workspaceId,
            deviceId: normalized.deviceId,
            userSessionId: normalized.userSessionId,
            origin: normalized.origin,
            expiresAt,
        },
    });
    return {
        handoffUrl,
        capabilityId,
        expiresAt,
        origin: normalized.origin,
        bridgeUrl: normalized.bridgeUrl,
        workspaceId: normalized.workspaceId,
        deviceId: normalized.deviceId,
        userSessionId: normalized.userSessionId,
        scopes: normalized.scopes,
    };
}
export function buildLocalUiHandoffUrl(input) {
    const url = parseUrl(input.uiUrl, "uiUrl");
    const encoded = encodeBase64Url(JSON.stringify(input.payload));
    url.hash = `patchTrailLocalHandoff=${encoded}`;
    return url.toString();
}
export function parseLocalUiHandoffFragment(handoffUrl) {
    const url = parseUrl(handoffUrl, "handoffUrl");
    const fragment = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
    const params = new URLSearchParams(fragment);
    const encoded = params.get("patchTrailLocalHandoff");
    if (!encoded)
        return undefined;
    const parsed = JSON.parse(decodeBase64Url(encoded));
    if (!isRecord(parsed) || parsed.version !== 1)
        return undefined;
    return {
        version: 1,
        bridgeUrl: readString(parsed.bridgeUrl, "bridgeUrl"),
        capabilityToken: readString(parsed.capabilityToken, "capabilityToken"),
        workspaceId: readString(parsed.workspaceId, "workspaceId"),
        deviceId: readString(parsed.deviceId, "deviceId"),
        userSessionId: readString(parsed.userSessionId, "userSessionId"),
        origin: readString(parsed.origin, "origin"),
        expiresAt: readString(parsed.expiresAt, "expiresAt"),
    };
}
function normalizeInput(input) {
    const bridgeUrl = normalizeBridgeUrl(input.bridgeUrl);
    const uiUrl = parseUrl(input.uiUrl, "uiUrl").toString();
    const origin = input.origin ?? new URL(uiUrl).origin;
    if (origin !== new URL(uiUrl).origin) {
        throw new LocalUiHandoffError("invalid_request", "The local UI handoff origin must exactly match the UI URL origin.");
    }
    if (!input.bridgeToken.trim()) {
        throw new LocalUiHandoffError("invalid_request", "A bridge capability token is required to mint a local UI handoff.");
    }
    if (!input.workspaceId.trim() ||
        !input.deviceId.trim() ||
        !input.userSessionId.trim()) {
        throw new LocalUiHandoffError("invalid_request", "workspaceId, deviceId, and userSessionId are required.");
    }
    const scopes = [...new Set(input.scopes)];
    if (scopes.length < 1) {
        throw new LocalUiHandoffError("invalid_request", "At least one local UI session scope is required.");
    }
    return {
        bridgeUrl,
        bridgeToken: input.bridgeToken,
        uiUrl,
        origin,
        workspaceId: input.workspaceId,
        deviceId: input.deviceId,
        userSessionId: input.userSessionId,
        scopes,
        ...(input.ttlSeconds ? { ttlSeconds: input.ttlSeconds } : {}),
        ...(input.sessionTtlSeconds
            ? { sessionTtlSeconds: input.sessionTtlSeconds }
            : {}),
    };
}
function normalizeBridgeUrl(value) {
    const url = parseUrl(value, "bridgeUrl");
    if (!isLoopbackBridgeUrl(url)) {
        throw new LocalUiHandoffError("invalid_request", "Bridge URL must point to a loopback host before the launcher can send the durable bridge token.");
    }
    url.pathname = "/";
    url.search = "";
    url.hash = "";
    return url.toString();
}
function isLoopbackBridgeUrl(url) {
    if (url.protocol !== "http:" && url.protocol !== "https:")
        return false;
    return (url.hostname === "localhost" ||
        url.hostname === "127.0.0.1" ||
        url.hostname === "::1" ||
        url.hostname === "[::1]");
}
async function readJsonObject(response) {
    const text = await response.text();
    if (!text)
        return {};
    const parsed = JSON.parse(text);
    if (!isRecord(parsed))
        return {};
    return parsed;
}
function readErrorMessage(payload) {
    return typeof payload.message === "string"
        ? payload.message
        : "The local PatchTrail bridge rejected the handoff request.";
}
function parseUrl(value, fieldName) {
    try {
        return new URL(value);
    }
    catch {
        throw new LocalUiHandoffError("invalid_request", `${fieldName} must be an absolute URL.`);
    }
}
function readString(value, fieldName) {
    if (typeof value === "string" && value.length > 0)
        return value;
    throw new LocalUiHandoffError("bridge_rejected", `The bridge response did not include ${fieldName}.`);
}
function isRecord(value) {
    return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function encodeBase64Url(value) {
    return Buffer.from(value, "utf8").toString("base64url");
}
function decodeBase64Url(value) {
    return Buffer.from(value, "base64url").toString("utf8");
}
