"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.StateShareClient = exports.StateShareServer = exports.RpcClient = exports.RpcServer = void 0;
exports.readSignedStream = readSignedStream;
exports.sign = sign;
const http_1 = require("http");
const util_1 = require("util");
const crypto_1 = __importDefault(require("crypto"));
const os_1 = __importDefault(require("os"));
const events_1 = require("events");
const fs_1 = require("fs");
const stream_1 = require("stream");
const path_1 = require("path");
async function readSignedStream(input, signature, hmacKey) {
    if (!signature)
        throw new Error('Missing signature');
    if (Array.isArray(signature))
        throw new Error('Multiple signatures');
    const verify = crypto_1.default.createHmac('sha256', hmacKey);
    const inputFromUtf8 = input.pipe(new stream_1.PassThrough()).setEncoding('utf8');
    input.pipe(verify);
    const verifyFinish = (0, events_1.once)(verify, 'finish');
    let body = '';
    for await (const chunk of inputFromUtf8) {
        body += chunk;
    }
    await verifyFinish;
    if (verify.read().toString('base64') !== signature) {
        throw new Error('Signature mismatch');
    }
    return body;
}
function sign(payload, hmacKey) {
    return crypto_1.default.createHmac('sha256', hmacKey).update(payload).digest('base64');
}
class RpcServer {
    constructor(serverPathPrefix) {
        this.serverPathPrefix = serverPathPrefix.replace(/[^a-zA-Z0-9]/g, '');
        this.hmacKey = undefined;
        this.server = undefined;
    }
    get handle() {
        return JSON.stringify([
            this.hmacKey.toString('base64'),
            this.server.address(),
        ]);
    }
    async _init() {
        this.server = (0, http_1.createServer)((req, res) => this._handleRequest(req, res));
        this.hmacKey = await (0, util_1.promisify)(crypto_1.default.randomBytes)(16);
        await this._serverListen();
    }
    async _serverListen() {
        if (this.cleanupCallback) {
            throw new Error('RPC server already listening');
        }
        let path;
        const name = `${this.serverPathPrefix}_${process.pid}_ipc_${RpcServer.ctr++}_${Math.floor(Math.random() * 10000)}`;
        if (process.platform === 'win32') {
            path = `\\\\.\\pipe\\${name}`;
        }
        else {
            const tmpdir = process.env.XDG_RUNTIME_DIR ?? os_1.default.tmpdir();
            path = (0, path_1.resolve)(`${tmpdir}/${name}.sock`);
        }
        this.server.listen({ path, exclusive: true });
        await (0, events_1.once)(this.server, 'listening');
        await fs_1.promises.chmod(path, 0o600);
        this.cleanupCallback = () => {
            if (process.platform !== 'win32') {
                try {
                    (0, fs_1.rmSync)(path);
                }
                catch {
                }
            }
        };
        process.on('exit', this.cleanupCallback);
    }
    async _handleRequest(req, res) {
        let response;
        try {
            const content = JSON.parse(await readSignedStream(req, req.headers['x-signature'], this.hmacKey));
            response = { status: 200, ...(await this.handleRpc(content)) };
        }
        catch (err) {
            response = {
                status: 500,
                error: err && typeof err === 'object' && 'message' in err
                    ? err.message
                    : String(err),
            };
        }
        res.statusCode = response.status;
        const payload = new TextEncoder().encode(JSON.stringify(response));
        res.setHeader('content-type', 'application/json');
        res.setHeader('content-length', payload.length);
        res.setHeader('x-signature', sign(payload, this.hmacKey));
        res.end(payload);
    }
    async close() {
        this.server.close();
        await (0, events_1.once)(this.server, 'close');
        this.cleanupCallback?.();
        this.cleanupCallback = undefined;
    }
}
exports.RpcServer = RpcServer;
RpcServer.ctr = 0;
class RpcClient {
    constructor(handle) {
        const [hmacKeyBase64, serverAddr] = JSON.parse(handle);
        this.serverAddress = serverAddr;
        this.hmacKey = Buffer.from(hmacKeyBase64, 'base64');
    }
    async makeRpcCall(reqData) {
        const payload = new TextEncoder().encode(JSON.stringify(reqData));
        const req = (0, http_1.request)({
            socketPath: this.serverAddress,
            method: 'POST',
            headers: {
                'content-type': 'application/json',
                'content-length': payload.length,
                'x-signature': sign(payload, this.hmacKey),
            },
        });
        req.end(payload);
        const [res] = (await (0, events_1.once)(req, 'response'));
        const content = JSON.parse(await readSignedStream(res, res.headers['x-signature'], this.hmacKey));
        if (res.statusCode !== 200 || content.status !== 200) {
            throw new Error(String(content.error ?? `${res.statusCode} ${res.statusMessage}`));
        }
        return content;
    }
}
exports.RpcClient = RpcClient;
class StateShareServer extends RpcServer {
    constructor(state) {
        super(state.productName);
        this.abortContexts = new Map();
        this.state = state;
    }
    static async create(state) {
        const result = new this(state);
        await result._init();
        return result;
    }
    async handleRpc(content) {
        switch (content.method) {
            case 'oidc:_abort':
                this.abortContexts.get(content.timeoutContextId)?.abort();
                return {};
            case 'oidc:OIDC_HUMAN_CALLBACK': {
                const abortController = new AbortController();
                this.abortContexts.set(content.timeoutContextId, abortController);
                try {
                    const result = await this.state.oidcPlugin.mongoClientOptions.authMechanismProperties.OIDC_HUMAN_CALLBACK({
                        ...content.callbackParams,
                        timeoutContext: abortController.signal,
                    });
                    return { result };
                }
                finally {
                    this.abortContexts.delete(content.timeoutContextId);
                }
            }
            default:
                throw new Error(`Unknown method ${JSON.stringify(content.method)}`);
        }
    }
}
exports.StateShareServer = StateShareServer;
class StateShareClient extends RpcClient {
    constructor(handle) {
        super(handle);
        this.oidcPlugin = {
            destroy() {
                return Promise.resolve();
            },
            serialize() {
                throw new Error('serialize() not supported in devtools-connect state-share clients');
            },
            logger: new events_1.EventEmitter(),
            mongoClientOptions: {
                authMechanismProperties: {
                    OIDC_HUMAN_CALLBACK: this._oidcCallback.bind(this, 'oidc:OIDC_HUMAN_CALLBACK'),
                },
            },
        };
    }
    async _oidcCallback(method, params) {
        const timeoutContextId = (await (0, util_1.promisify)(crypto_1.default.randomBytes)(16)).toString('base64');
        const { timeoutContext, ...callbackParams } = params;
        const abort = async () => {
            await this.makeRpcCall({
                method: 'oidc:_abort',
                timeoutContextId,
            });
        };
        timeoutContext?.addEventListener('abort', abort);
        try {
            const { result } = await this.makeRpcCall({
                method,
                timeoutContextId,
                callbackParams,
            });
            return result;
        }
        finally {
            timeoutContext?.removeEventListener('abort', abort);
        }
    }
}
exports.StateShareClient = StateShareClient;
//# sourceMappingURL=ipc-rpc-state-share.js.map