/**
 * LocaltunnelProvider — Tier-2 consent-gated relay tunnel via *.loca.lt.
 *
 * Per spec specs/dev-infrastructure/tunnel-failure-resilience.md Part 1
 * (provider abstraction, Tier 2) and Part 7 (supply-chain hardening).
 *
 * SECURITY POSTURE:
 *   - localtunnel routes the agent's local server traffic through
 *     loca.lt servers. The relay operator (and anyone who logs the URL)
 *     can see dashboard PIN + signed view URLs while the relay is active.
 *     This is exactly why the spec gates Tier-2 providers behind owner
 *     consent — see Part 3 (consent flow) and Part 6 (mandatory
 *     credential rotation on relay-episode end).
 *   - The agent never spins this up without an explicit per-episode
 *     consent record bound to the owner principal — that's enforced in
 *     TunnelManager's state machine, not in this provider.
 *
 * Supply-chain hardening (Part 7):
 *   - The npm package `localtunnel` is pinned at exact version in
 *     package.json (no caret range).
 *   - The provider spawns the localtunnel client via its programmatic
 *     API; a future hardening PR can switch to child-process isolation
 *     once the spec's checksum-verified install path is added.
 *   - `isAvailable()` resolves false if the npm package cannot be
 *     loaded, so a missing dep degrades gracefully (provider skipped)
 *     instead of crashing.
 *
 * The provider does NOT own retry/reconnect/notification — those are
 * the manager's responsibility per the single-owner mandate (PR 2).
 */
export class LocaltunnelProvider {
    name = 'localtunnel';
    tier = 2;
    port;
    startTimeoutMs;
    subdomain;
    /**
     * Cached dynamic-import result. The first `isAvailable()` call
     * detects whether the npm package is present; subsequent calls reuse
     * the cached resolution.
     */
    _moduleResolved;
    constructor(opts) {
        this.port = opts.port;
        this.startTimeoutMs = opts.startTimeoutMs ?? 20_000;
        this.subdomain = opts.subdomain;
    }
    async isAvailable() {
        const mod = await this.loadModule();
        return mod !== 'unavailable';
    }
    async start(localPort) {
        const mod = await this.loadModule();
        if (mod === 'unavailable') {
            throw new Error('binary-missing: localtunnel npm package is not installed');
        }
        const lt = mod.default;
        if (typeof lt !== 'function') {
            throw new Error('binary-missing: localtunnel module did not expose the expected factory');
        }
        const port = localPort || this.port;
        return new Promise((resolve, reject) => {
            let resolved = false;
            const timeout = setTimeout(() => {
                if (resolved)
                    return;
                resolved = true;
                reject(new Error('timeout: localtunnel did not emit a URL within the start budget'));
            }, this.startTimeoutMs);
            lt({ port, subdomain: this.subdomain }).then((client) => {
                if (resolved) {
                    try {
                        client.close();
                    }
                    catch { /* best effort */ }
                    return;
                }
                resolved = true;
                clearTimeout(timeout);
                if (!client.url) {
                    reject(new Error('process-exit: localtunnel returned no URL'));
                    return;
                }
                client.on('error', () => { });
                resolve({
                    url: client.url,
                    stop: async () => {
                        try {
                            client.close();
                        }
                        catch { /* already dead */ }
                    },
                });
            }, (err) => {
                if (resolved)
                    return;
                resolved = true;
                clearTimeout(timeout);
                const msg = (err?.message ?? '').toLowerCase();
                if (msg.includes('rate') || msg.includes('429')) {
                    reject(new Error(`rate-limited: localtunnel: ${err.message}`));
                    return;
                }
                if (msg.includes('econnrefused') || msg.includes('dns')) {
                    reject(new Error(`network: localtunnel: ${err.message}`));
                    return;
                }
                reject(new Error(`process-exit: localtunnel start failed: ${err.message}`));
            });
        });
    }
    async loadModule() {
        if (this._moduleResolved !== undefined)
            return this._moduleResolved;
        try {
            // Dynamic specifier so TypeScript doesn't require an installed
            // `@types/localtunnel`. The npm package itself is an optional
            // peer-style dep — agents without it installed degrade
            // gracefully via the catch below.
            const specifier = 'localtunnel';
            const mod = (await import(/* @vite-ignore */ specifier));
            this._moduleResolved = mod;
        }
        catch {
            this._moduleResolved = 'unavailable';
        }
        return this._moduleResolved;
    }
}
//# sourceMappingURL=LocaltunnelProvider.js.map