"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FunctionsEmulator = void 0;
const fs = require("fs");
const path = require("path");
const express = require("express");
const clc = require("colorette");
const http = require("http");
const jwt = require("jsonwebtoken");
const cors = require("cors");
const url_1 = require("url");
const events_1 = require("events");
const logger_1 = require("../logger");
const track_1 = require("../track");
const constants_1 = require("./constants");
const types_1 = require("./types");
const chokidar = require("chokidar");
const spawn = require("cross-spawn");
const functionsEmulatorShared_1 = require("./functionsEmulatorShared");
const registry_1 = require("./registry");
const emulatorLogger_1 = require("./emulatorLogger");
const functionsRuntimeWorker_1 = require("./functionsRuntimeWorker");
const error_1 = require("../error");
const workQueue_1 = require("./workQueue");
const utils_1 = require("../utils");
const defaultCredentials_1 = require("../defaultCredentials");
const adminSdkConfig_1 = require("./adminSdkConfig");
const validate_1 = require("../deploy/functions/validate");
const secretManager_1 = require("../gcp/secretManager");
const runtimes = require("../deploy/functions/runtimes");
const backend = require("../deploy/functions/backend");
const functionsEnv = require("../functions/env");
const v1_1 = require("../functions/events/v1");
const build_1 = require("../deploy/functions/build");
const env_1 = require("./env");
const EVENT_INVOKE = "functions:invoke";
const EVENT_INVOKE_GA4 = "functions_invoke";
const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$");
class FunctionsEmulator {
    static getHttpFunctionUrl(projectId, name, region, info) {
        let url;
        if (info) {
            url = new url_1.URL("http://" + (0, functionsEmulatorShared_1.formatHost)(info));
        }
        else {
            url = registry_1.EmulatorRegistry.url(types_1.Emulators.FUNCTIONS);
        }
        url.pathname = `/${projectId}/${region}/${name}`;
        return url.toString();
    }
    constructor(args) {
        this.args = args;
        this.triggers = {};
        this.triggerGeneration = 0;
        this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.FUNCTIONS);
        this.multicastTriggers = {};
        this.blockingFunctionsConfig = {};
        emulatorLogger_1.EmulatorLogger.verbosity = this.args.quiet ? emulatorLogger_1.Verbosity.QUIET : emulatorLogger_1.Verbosity.DEBUG;
        if (this.args.debugPort) {
            this.args.disabledRuntimeFeatures = this.args.disabledRuntimeFeatures || {};
            this.args.disabledRuntimeFeatures.timeout = true;
        }
        this.adminSdkConfig = Object.assign(Object.assign({}, this.args.adminSdkConfig), { projectId: this.args.projectId });
        const mode = this.args.debugPort
            ? types_1.FunctionsExecutionMode.SEQUENTIAL
            : types_1.FunctionsExecutionMode.AUTO;
        this.workerPools = {};
        for (const backend of this.args.emulatableBackends) {
            const pool = new functionsRuntimeWorker_1.RuntimeWorkerPool(mode);
            this.workerPools[backend.codebase] = pool;
        }
        this.workQueue = new workQueue_1.WorkQueue(mode);
    }
    async getCredentialsEnvironment() {
        const credentialEnv = {};
        if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
            this.logger.logLabeled("WARN", "functions", `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`);
        }
        else if (this.args.account) {
            const defaultCredPath = await (0, defaultCredentials_1.getCredentialPathAsync)(this.args.account);
            if (defaultCredPath) {
                this.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
                credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
            }
        }
        else {
            this.logger.logLabeled("WARN", "functions", "You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services.");
        }
        return credentialEnv;
    }
    createHubServer() {
        this.workQueue.start();
        const hub = express();
        const dataMiddleware = (req, res, next) => {
            const chunks = [];
            req.on("data", (chunk) => {
                chunks.push(chunk);
            });
            req.on("end", () => {
                req.rawBody = Buffer.concat(chunks);
                next();
            });
        };
        const backgroundFunctionRoute = `/functions/projects/:project_id/triggers/:trigger_name(*)`;
        const httpsFunctionRoute = `/${this.args.projectId}/:region/:trigger_name`;
        const multicastFunctionRoute = `/functions/projects/:project_id/trigger_multicast`;
        const httpsFunctionRoutes = [httpsFunctionRoute, `${httpsFunctionRoute}/*`];
        const listBackendsRoute = `/backends`;
        const httpsHandler = (req, res) => {
            const work = () => {
                return this.handleHttpsTrigger(req, res);
            };
            work.type = `${req.path}-${new Date().toISOString()}`;
            this.workQueue.submit(work);
        };
        const multicastHandler = (req, res) => {
            var _a;
            const projectId = req.params.project_id;
            const rawBody = req.rawBody;
            const event = JSON.parse(rawBody.toString());
            let triggerKey;
            if ((_a = req.headers["content-type"]) === null || _a === void 0 ? void 0 : _a.includes("cloudevent")) {
                triggerKey = `${this.args.projectId}:${event.type}`;
            }
            else {
                triggerKey = `${this.args.projectId}:${event.eventType}`;
            }
            if (event.data.bucket) {
                triggerKey += `:${event.data.bucket}`;
            }
            const triggers = this.multicastTriggers[triggerKey] || [];
            const { host, port } = this.getInfo();
            triggers.forEach((triggerId) => {
                const work = () => {
                    return new Promise((resolve, reject) => {
                        const trigReq = http.request({
                            host: (0, utils_1.connectableHostname)(host),
                            port,
                            method: req.method,
                            path: `/functions/projects/${projectId}/triggers/${triggerId}`,
                            headers: req.headers,
                        });
                        trigReq.on("error", reject);
                        trigReq.write(rawBody);
                        trigReq.end();
                        resolve();
                    });
                };
                work.type = `${triggerId}-${new Date().toISOString()}`;
                this.workQueue.submit(work);
            });
            res.json({ status: "multicast_acknowledged" });
        };
        const listBackendsHandler = (req, res) => {
            res.json({ backends: this.getBackendInfo() });
        };
        hub.get(listBackendsRoute, cors({ origin: true }), listBackendsHandler);
        hub.post(backgroundFunctionRoute, dataMiddleware, httpsHandler);
        hub.post(multicastFunctionRoute, dataMiddleware, multicastHandler);
        hub.all(httpsFunctionRoutes, dataMiddleware, httpsHandler);
        hub.all("*", dataMiddleware, (req, res) => {
            logger_1.logger.debug(`Functions emulator received unknown request at path ${req.path}`);
            res.sendStatus(404);
        });
        return hub;
    }
    async sendRequest(trigger, body) {
        const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
        const pool = this.workerPools[record.backend.codebase];
        if (!pool.readyForWork(trigger.id)) {
            await this.startRuntime(record.backend, trigger);
        }
        const worker = pool.getIdleWorker(trigger.id);
        const reqBody = JSON.stringify(body);
        const headers = {
            "Content-Type": "application/json",
            "Content-Length": `${reqBody.length}`,
        };
        return new Promise((resolve, reject) => {
            const req = http.request({
                path: `/`,
                socketPath: worker.runtime.socketPath,
                headers: headers,
            }, resolve);
            req.on("error", reject);
            req.write(reqBody);
            req.end();
        });
    }
    async start() {
        for (const backend of this.args.emulatableBackends) {
            backend.nodeBinary = this.getNodeBinary(backend);
        }
        const credentialEnv = await this.getCredentialsEnvironment();
        for (const e of this.args.emulatableBackends) {
            e.env = Object.assign(Object.assign({}, credentialEnv), e.env);
        }
        if (Object.keys(this.adminSdkConfig || {}).length <= 1) {
            const adminSdkConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(this.args.projectId);
            if (adminSdkConfig) {
                this.adminSdkConfig = adminSdkConfig;
            }
            else {
                this.logger.logLabeled("WARN", "functions", "Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.");
                this.adminSdkConfig = (0, adminSdkConfig_1.constructDefaultAdminSdkConfig)(this.args.projectId);
            }
        }
        const { host, port } = this.getInfo();
        this.workQueue.start();
        const server = this.createHubServer().listen(port, host);
        this.destroyServer = (0, utils_1.createDestroyer)(server);
        return Promise.resolve();
    }
    async connect() {
        for (const backend of this.args.emulatableBackends) {
            this.logger.logLabeled("BULLET", "functions", `Watching "${backend.functionsDir}" for Cloud Functions...`);
            const watcher = chokidar.watch(backend.functionsDir, {
                ignored: [
                    /.+?[\\\/]node_modules[\\\/].+?/,
                    /(^|[\/\\])\../,
                    /.+\.log/,
                ],
                persistent: true,
            });
            const debouncedLoadTriggers = (0, utils_1.debounce)(() => this.loadTriggers(backend), 1000);
            watcher.on("change", (filePath) => {
                this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`);
                return debouncedLoadTriggers();
            });
            await this.loadTriggers(backend, true);
        }
        await this.performPostLoadOperations();
        return;
    }
    async stop() {
        try {
            await this.workQueue.flush();
        }
        catch (e) {
            this.logger.logLabeled("WARN", "functions", "Functions emulator work queue did not empty before stopping");
        }
        this.workQueue.stop();
        for (const pool of Object.values(this.workerPools)) {
            pool.exit();
        }
        if (this.destroyServer) {
            await this.destroyServer();
        }
    }
    async discoverTriggers(emulatableBackend) {
        if (emulatableBackend.predefinedTriggers) {
            return (0, functionsEmulatorShared_1.emulatedFunctionsByRegion)(emulatableBackend.predefinedTriggers, emulatableBackend.secretEnv);
        }
        else {
            const runtimeConfig = this.getRuntimeConfig(emulatableBackend);
            const runtimeDelegateContext = {
                projectId: this.args.projectId,
                projectDir: this.args.projectDir,
                sourceDir: emulatableBackend.functionsDir,
            };
            if (emulatableBackend.nodeMajorVersion) {
                runtimeDelegateContext.runtime = `nodejs${emulatableBackend.nodeMajorVersion}`;
            }
            const runtimeDelegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext);
            logger_1.logger.debug(`Validating ${runtimeDelegate.name} source`);
            await runtimeDelegate.validate();
            logger_1.logger.debug(`Building ${runtimeDelegate.name} source`);
            await runtimeDelegate.build();
            const firebaseConfig = this.getFirebaseConfig();
            const environment = Object.assign(Object.assign(Object.assign(Object.assign({}, this.getSystemEnvs()), this.getEmulatorEnvs()), { FIREBASE_CONFIG: firebaseConfig }), emulatableBackend.env);
            const userEnvOpt = {
                functionsSource: emulatableBackend.functionsDir,
                projectId: this.args.projectId,
                projectAlias: this.args.projectAlias,
            };
            const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
            const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment);
            const resolution = await (0, build_1.resolveBackend)(discoveredBuild, JSON.parse(firebaseConfig), userEnvOpt, userEnvs);
            const discoveredBackend = resolution.backend;
            const endpoints = backend.allEndpoints(discoveredBackend);
            (0, functionsEmulatorShared_1.prepareEndpoints)(endpoints);
            for (const e of endpoints) {
                e.codebase = emulatableBackend.codebase;
            }
            return (0, functionsEmulatorShared_1.emulatedFunctionsFromEndpoints)(endpoints);
        }
    }
    async loadTriggers(emulatableBackend, force = false) {
        if (!emulatableBackend.nodeBinary) {
            throw new error_1.FirebaseError(`No node binary for ${emulatableBackend.functionsDir}. This should never happen.`);
        }
        let triggerDefinitions = [];
        try {
            triggerDefinitions = await this.discoverTriggers(emulatableBackend);
            this.logger.logLabeled("SUCCESS", "functions", `Loaded functions definitions from source: ${triggerDefinitions
                .map((t) => t.entryPoint)
                .join(", ")}.`);
        }
        catch (e) {
            this.logger.logLabeled("ERROR", "functions", `Failed to load function definition from source: ${e}`);
            return;
        }
        this.workerPools[emulatableBackend.codebase].refresh();
        this.blockingFunctionsConfig = {};
        const toSetup = triggerDefinitions.filter((definition) => {
            if (force) {
                return true;
            }
            const anyEnabledMatch = Object.values(this.triggers).some((record) => {
                const sameEntryPoint = record.def.entryPoint === definition.entryPoint;
                const sameEventTrigger = JSON.stringify(record.def.eventTrigger) === JSON.stringify(definition.eventTrigger);
                if (sameEntryPoint && !sameEventTrigger) {
                    this.logger.log("DEBUG", `Definition for trigger ${definition.entryPoint} changed from ${JSON.stringify(record.def.eventTrigger)} to ${JSON.stringify(definition.eventTrigger)}`);
                }
                return record.enabled && sameEntryPoint && sameEventTrigger;
            });
            return !anyEnabledMatch;
        });
        for (const definition of toSetup) {
            try {
                (0, validate_1.functionIdsAreValid)([Object.assign(Object.assign({}, definition), { id: definition.name })]);
            }
            catch (e) {
                throw new error_1.FirebaseError(`functions[${definition.id}]: Invalid function id: ${e.message}`);
            }
            let added = false;
            let url = undefined;
            if (definition.httpsTrigger) {
                added = true;
                url = FunctionsEmulator.getHttpFunctionUrl(this.args.projectId, definition.name, definition.region);
            }
            else if (definition.eventTrigger) {
                const service = (0, functionsEmulatorShared_1.getFunctionService)(definition);
                const key = this.getTriggerKey(definition);
                const signature = (0, functionsEmulatorShared_1.getSignatureType)(definition);
                switch (service) {
                    case constants_1.Constants.SERVICE_FIRESTORE:
                        added = await this.addFirestoreTrigger(this.args.projectId, key, definition.eventTrigger);
                        break;
                    case constants_1.Constants.SERVICE_REALTIME_DATABASE:
                        added = await this.addRealtimeDatabaseTrigger(this.args.projectId, definition.id, key, definition.eventTrigger, signature, definition.region);
                        break;
                    case constants_1.Constants.SERVICE_PUBSUB:
                        added = await this.addPubsubTrigger(definition.name, key, definition.eventTrigger, signature, definition.schedule);
                        break;
                    case constants_1.Constants.SERVICE_EVENTARC:
                        added = await this.addEventarcTrigger(this.args.projectId, key, definition.eventTrigger);
                        break;
                    case constants_1.Constants.SERVICE_AUTH:
                        added = this.addAuthTrigger(this.args.projectId, key, definition.eventTrigger);
                        break;
                    case constants_1.Constants.SERVICE_STORAGE:
                        added = this.addStorageTrigger(this.args.projectId, key, definition.eventTrigger);
                        break;
                    default:
                        this.logger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`);
                        break;
                }
            }
            else if (definition.blockingTrigger) {
                url = FunctionsEmulator.getHttpFunctionUrl(this.args.projectId, definition.name, definition.region);
                added = this.addBlockingTrigger(url, definition.blockingTrigger);
            }
            else {
                this.logger.log("WARN", `Unsupported function type on ${definition.name}. Expected either an httpsTrigger, eventTrigger, or blockingTrigger.`);
            }
            const ignored = !added;
            this.addTriggerRecord(definition, { backend: emulatableBackend, ignored, url });
            const type = definition.httpsTrigger
                ? "http"
                : constants_1.Constants.getServiceName((0, functionsEmulatorShared_1.getFunctionService)(definition));
            if (ignored) {
                const msg = `function ignored because the ${type} emulator does not exist or is not running.`;
                this.logger.logLabeled("BULLET", `functions[${definition.id}]`, msg);
            }
            else {
                const msg = url
                    ? `${clc.bold(type)} function initialized (${url}).`
                    : `${clc.bold(type)} function initialized.`;
                this.logger.logLabeled("SUCCESS", `functions[${definition.id}]`, msg);
            }
        }
        if (this.args.debugPort) {
            emulatableBackend.secretEnv = Object.values(toSetup.reduce((acc, curr) => {
                for (const secret of curr.secretEnvironmentVariables || []) {
                    acc[secret.key] = secret;
                }
                return acc;
            }, {}));
            await this.startRuntime(emulatableBackend);
        }
    }
    addEventarcTrigger(projectId, key, eventTrigger) {
        if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.EVENTARC)) {
            return Promise.resolve(false);
        }
        const bundle = {
            eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "eventarc.googleapis.com" }),
        };
        logger_1.logger.debug(`addEventarcTrigger`, JSON.stringify(bundle));
        return registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC)
            .post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle)
            .then(() => true)
            .catch((err) => {
            this.logger.log("WARN", "Error adding Eventarc function: " + err);
            return false;
        });
    }
    async performPostLoadOperations() {
        if (!this.blockingFunctionsConfig.triggers &&
            !this.blockingFunctionsConfig.forwardInboundCredentials) {
            return;
        }
        if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.AUTH)) {
            return;
        }
        const path = `/identitytoolkit.googleapis.com/v2/projects/${this.getProjectId()}/config?updateMask=blockingFunctions`;
        try {
            const client = registry_1.EmulatorRegistry.client(types_1.Emulators.AUTH);
            await client.patch(path, { blockingFunctions: this.blockingFunctionsConfig }, {
                headers: { Authorization: "Bearer owner" },
            });
        }
        catch (err) {
            this.logger.log("WARN", "Error updating blocking functions config to the auth emulator: " + err);
            throw err;
        }
    }
    getV1DatabaseApiAttributes(projectId, key, eventTrigger) {
        const result = DATABASE_PATH_PATTERN.exec(eventTrigger.resource);
        if (result === null || result.length !== 3) {
            this.logger.log("WARN", `Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource}`);
            throw new error_1.FirebaseError(`Event function ${key} has malformed resource member`);
        }
        const instance = result[1];
        const bundle = JSON.stringify({
            name: `projects/${projectId}/locations/_/functions/${key}`,
            path: result[2],
            event: eventTrigger.eventType,
            topic: `projects/${projectId}/topics/${key}`,
        });
        let apiPath = "/.settings/functionTriggers.json";
        if (instance !== "") {
            apiPath += `?ns=${instance}`;
        }
        else {
            this.logger.log("WARN", `No project in use. Registering function for sentinel namespace '${constants_1.Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'`);
        }
        return { bundle, apiPath, instance };
    }
    getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region) {
        var _a, _b, _c;
        const instance = ((_a = eventTrigger.eventFilters) === null || _a === void 0 ? void 0 : _a.instance) || ((_b = eventTrigger.eventFilterPathPatterns) === null || _b === void 0 ? void 0 : _b.instance);
        if (!instance) {
            throw new error_1.FirebaseError("A database instance must be supplied.");
        }
        const ref = (_c = eventTrigger.eventFilterPathPatterns) === null || _c === void 0 ? void 0 : _c.ref;
        if (!ref) {
            throw new error_1.FirebaseError("A database reference must be supplied.");
        }
        if (region !== "us-central1") {
            this.logger.logLabeled("WARN", `functions[${id}]`, `function region is defined outside the database region, will not trigger.`);
        }
        const bundle = JSON.stringify({
            name: `projects/${projectId}/locations/${region}/triggers/${key}`,
            path: ref,
            event: eventTrigger.eventType,
            topic: `projects/${projectId}/topics/${key}`,
            namespacePattern: instance,
        });
        const apiPath = "/.settings/functionTriggers.json";
        return { bundle, apiPath, instance };
    }
    async addRealtimeDatabaseTrigger(projectId, id, key, eventTrigger, signature, region) {
        if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.DATABASE)) {
            return false;
        }
        const { bundle, apiPath, instance } = signature === "cloudevent"
            ? this.getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region)
            : this.getV1DatabaseApiAttributes(projectId, key, eventTrigger);
        logger_1.logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle));
        const client = registry_1.EmulatorRegistry.client(types_1.Emulators.DATABASE);
        try {
            await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } });
        }
        catch (err) {
            this.logger.log("WARN", "Error adding Realtime Database function: " + err);
            throw err;
        }
        return true;
    }
    async addFirestoreTrigger(projectId, key, eventTrigger) {
        if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.FIRESTORE)) {
            return Promise.resolve(false);
        }
        const bundle = JSON.stringify({
            eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "firestore.googleapis.com" }),
        });
        logger_1.logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle));
        const client = registry_1.EmulatorRegistry.client(types_1.Emulators.FIRESTORE);
        try {
            await client.put(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle);
        }
        catch (err) {
            this.logger.log("WARN", "Error adding firestore function: " + err);
            throw err;
        }
        return true;
    }
    async addPubsubTrigger(triggerName, key, eventTrigger, signatureType, schedule) {
        const pubsubEmulator = registry_1.EmulatorRegistry.get(types_1.Emulators.PUBSUB);
        if (!pubsubEmulator) {
            return false;
        }
        logger_1.logger.debug(`addPubsubTrigger`, JSON.stringify({ eventTrigger }));
        const resource = eventTrigger.resource;
        let topic;
        if (schedule) {
            topic = "firebase-schedule-" + triggerName;
        }
        else {
            const resourceParts = resource.split("/");
            topic = resourceParts[resourceParts.length - 1];
        }
        try {
            await pubsubEmulator.addTrigger(topic, key, signatureType);
            return true;
        }
        catch (e) {
            return false;
        }
    }
    addAuthTrigger(projectId, key, eventTrigger) {
        logger_1.logger.debug(`addAuthTrigger`, JSON.stringify({ eventTrigger }));
        const eventTriggerId = `${projectId}:${eventTrigger.eventType}`;
        const triggers = this.multicastTriggers[eventTriggerId] || [];
        triggers.push(key);
        this.multicastTriggers[eventTriggerId] = triggers;
        return true;
    }
    addStorageTrigger(projectId, key, eventTrigger) {
        logger_1.logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger }));
        const bucket = eventTrigger.resource.startsWith("projects/_/buckets/")
            ? eventTrigger.resource.split("/")[3]
            : eventTrigger.resource;
        const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`;
        const triggers = this.multicastTriggers[eventTriggerId] || [];
        triggers.push(key);
        this.multicastTriggers[eventTriggerId] = triggers;
        return true;
    }
    addBlockingTrigger(url, blockingTrigger) {
        logger_1.logger.debug(`addBlockingTrigger`, JSON.stringify({ blockingTrigger }));
        const eventType = blockingTrigger.eventType;
        if (!v1_1.AUTH_BLOCKING_EVENTS.includes(eventType)) {
            return false;
        }
        if (blockingTrigger.eventType === v1_1.BEFORE_CREATE_EVENT) {
            this.blockingFunctionsConfig.triggers = Object.assign(Object.assign({}, this.blockingFunctionsConfig.triggers), { beforeCreate: {
                    functionUri: url,
                } });
        }
        else {
            this.blockingFunctionsConfig.triggers = Object.assign(Object.assign({}, this.blockingFunctionsConfig.triggers), { beforeSignIn: {
                    functionUri: url,
                } });
        }
        this.blockingFunctionsConfig.forwardInboundCredentials = {
            accessToken: !!blockingTrigger.options.accessToken,
            idToken: !!blockingTrigger.options.idToken,
            refreshToken: !!blockingTrigger.options.refreshToken,
        };
        return true;
    }
    getProjectId() {
        return this.args.projectId;
    }
    getInfo() {
        const host = this.args.host || constants_1.Constants.getDefaultHost();
        const port = this.args.port || constants_1.Constants.getDefaultPort(types_1.Emulators.FUNCTIONS);
        return {
            name: this.getName(),
            host,
            port,
        };
    }
    getName() {
        return types_1.Emulators.FUNCTIONS;
    }
    getTriggerDefinitions() {
        return Object.values(this.triggers).map((record) => record.def);
    }
    getTriggerRecordByKey(triggerKey) {
        const record = this.triggers[triggerKey];
        if (!record) {
            logger_1.logger.debug(`Could not find key=${triggerKey} in ${JSON.stringify(this.triggers)}`);
            throw new error_1.FirebaseError(`No function with key ${triggerKey}`);
        }
        return record;
    }
    getTriggerKey(def) {
        if (def.eventTrigger) {
            const triggerKey = `${def.id}-${this.triggerGeneration}`;
            return def.eventTrigger.channel ? `${triggerKey}-${def.eventTrigger.channel}` : triggerKey;
        }
        else {
            return def.id;
        }
    }
    getBackendInfo() {
        const cf3Triggers = this.getCF3Triggers();
        return this.args.emulatableBackends.map((e) => {
            return (0, functionsEmulatorShared_1.toBackendInfo)(e, cf3Triggers);
        });
    }
    getCF3Triggers() {
        return Object.values(this.triggers)
            .filter((t) => !t.backend.extensionInstanceId)
            .map((t) => t.def);
    }
    addTriggerRecord(def, opts) {
        const key = this.getTriggerKey(def);
        this.triggers[key] = {
            def,
            enabled: true,
            backend: opts.backend,
            ignored: opts.ignored,
            url: opts.url,
        };
    }
    setTriggersForTesting(triggers, backend) {
        this.triggers = {};
        triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false }));
    }
    getNodeBinary(backend) {
        const pkg = require(path.join(backend.functionsDir, "package.json"));
        if ((!pkg.engines || !pkg.engines.node) && !backend.nodeMajorVersion) {
            this.logger.log("WARN", `Your functions directory ${backend.functionsDir} does not specify a Node version.\n   ` +
                "- Learn more at https://firebase.google.com/docs/functions/manage-functions#set_runtime_options");
            return process.execPath;
        }
        const hostMajorVersion = process.versions.node.split(".")[0];
        const requestedMajorVersion = backend.nodeMajorVersion
            ? `${backend.nodeMajorVersion}`
            : pkg.engines.node;
        let localMajorVersion = "0";
        const localNodePath = path.join(backend.functionsDir, "node_modules/.bin/node");
        try {
            const localNodeOutput = spawn.sync(localNodePath, ["--version"]).stdout.toString();
            localMajorVersion = localNodeOutput.slice(1).split(".")[0];
        }
        catch (err) {
        }
        if (requestedMajorVersion === localMajorVersion) {
            this.logger.logLabeled("SUCCESS", "functions", `Using node@${requestedMajorVersion} from local cache.`);
            return localNodePath;
        }
        if (requestedMajorVersion === hostMajorVersion) {
            this.logger.logLabeled("SUCCESS", "functions", `Using node@${requestedMajorVersion} from host.`);
        }
        else {
            if (process.env.FIREPIT_VERSION) {
                this.logger.log("WARN", `You've requested "node" version "${requestedMajorVersion}", but the standalone Firebase CLI comes with bundled Node "${hostMajorVersion}".`);
                this.logger.log("INFO", `To use a different Node.js version, consider removing the standalone Firebase CLI and switching to "firebase-tools" on npm.`);
            }
            else {
                this.logger.log("WARN", `Your requested "node" version "${requestedMajorVersion}" doesn't match your global version "${hostMajorVersion}". Using node@${hostMajorVersion} from host.`);
            }
        }
        return process.execPath;
    }
    getRuntimeConfig(backend) {
        const configPath = `${backend.functionsDir}/.runtimeconfig.json`;
        try {
            const configContent = fs.readFileSync(configPath, "utf8");
            return JSON.parse(configContent.toString());
        }
        catch (e) {
        }
        return {};
    }
    getUserEnvs(backend) {
        const projectInfo = {
            functionsSource: backend.functionsDir,
            projectId: this.args.projectId,
            projectAlias: this.args.projectAlias,
            isEmulator: true,
        };
        if (functionsEnv.hasUserEnvs(projectInfo)) {
            try {
                return functionsEnv.loadUserEnvs(projectInfo);
            }
            catch (e) {
                logger_1.logger.debug("Failed to load local environment variables", e);
            }
        }
        return {};
    }
    getSystemEnvs(trigger) {
        const envs = {};
        envs.GCLOUD_PROJECT = this.args.projectId;
        envs.K_REVISION = "1";
        envs.PORT = "80";
        if (trigger === null || trigger === void 0 ? void 0 : trigger.timeoutSeconds) {
            envs.FUNCTIONS_EMULATOR_TIMEOUT_SECONDS = trigger.timeoutSeconds.toString();
        }
        if (trigger) {
            const target = trigger.entryPoint;
            envs.FUNCTION_TARGET = target;
            envs.FUNCTION_SIGNATURE_TYPE = (0, functionsEmulatorShared_1.getSignatureType)(trigger);
            envs.K_SERVICE = trigger.name;
        }
        return envs;
    }
    getEmulatorEnvs() {
        const envs = {};
        envs.FUNCTIONS_EMULATOR = "true";
        envs.TZ = "UTC";
        envs.FIREBASE_DEBUG_MODE = "true";
        envs.FIREBASE_DEBUG_FEATURES = JSON.stringify({
            skipTokenVerification: true,
            enableCors: true,
        });
        let emulatorInfos = registry_1.EmulatorRegistry.listRunningWithInfo();
        if (this.args.remoteEmulators) {
            emulatorInfos = emulatorInfos.concat(Object.values(this.args.remoteEmulators));
        }
        (0, env_1.setEnvVarsForEmulators)(envs, emulatorInfos);
        if (this.args.debugPort) {
            envs["FUNCTION_DEBUG_MODE"] = "true";
        }
        return envs;
    }
    getFirebaseConfig() {
        const databaseEmulator = this.getEmulatorInfo(types_1.Emulators.DATABASE);
        let emulatedDatabaseURL = undefined;
        if (databaseEmulator) {
            let ns = this.args.projectId;
            if (this.adminSdkConfig.databaseURL) {
                const asUrl = new url_1.URL(this.adminSdkConfig.databaseURL);
                ns = asUrl.hostname.split(".")[0];
            }
            emulatedDatabaseURL = `http://${(0, functionsEmulatorShared_1.formatHost)(databaseEmulator)}/?ns=${ns}`;
        }
        return JSON.stringify({
            storageBucket: this.adminSdkConfig.storageBucket,
            databaseURL: emulatedDatabaseURL || this.adminSdkConfig.databaseURL,
            projectId: this.args.projectId,
        });
    }
    getRuntimeEnvs(backend, trigger) {
        return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, this.getUserEnvs(backend)), this.getSystemEnvs(trigger)), this.getEmulatorEnvs()), { FIREBASE_CONFIG: this.getFirebaseConfig() }), backend.env);
    }
    async resolveSecretEnvs(backend, trigger) {
        let secretEnvs = {};
        const secretPath = (0, functionsEmulatorShared_1.getSecretLocalPath)(backend, this.args.projectDir);
        try {
            const data = fs.readFileSync(secretPath, "utf8");
            secretEnvs = functionsEnv.parseStrict(data);
        }
        catch (e) {
            if (e.code !== "ENOENT") {
                this.logger.logLabeled("ERROR", "functions", `Failed to read local secrets file ${secretPath}: ${e.message}`);
            }
        }
        const secrets = (trigger === null || trigger === void 0 ? void 0 : trigger.secretEnvironmentVariables) || backend.secretEnv;
        const accesses = secrets
            .filter((s) => !secretEnvs[s.key])
            .map(async (s) => {
            var _a;
            this.logger.logLabeled("INFO", "functions", `Trying to access secret ${s.secret}@latest`);
            const value = await (0, secretManager_1.accessSecretVersion)(this.getProjectId(), s.secret, (_a = s.version) !== null && _a !== void 0 ? _a : "latest");
            return [s.key, value];
        });
        const accessResults = await (0, utils_1.allSettled)(accesses);
        const errs = [];
        for (const result of accessResults) {
            if (result.status === "rejected") {
                errs.push(result.reason);
            }
            else {
                const [k, v] = result.value;
                secretEnvs[k] = v;
            }
        }
        if (errs.length > 0) {
            this.logger.logLabeled("ERROR", "functions", "Unable to access secret environment variables from Google Cloud Secret Manager. " +
                "Make sure the credential used for the Functions Emulator have access " +
                `or provide override values in ${secretPath}:\n\t` +
                errs.join("\n\t"));
        }
        return secretEnvs;
    }
    async startRuntime(backend, trigger) {
        var _a;
        const emitter = new events_1.EventEmitter();
        const args = [path.join(__dirname, "functionsEmulatorRuntime")];
        if (this.args.debugPort) {
            if (process.env.FIREPIT_VERSION && process.execPath === backend.nodeBinary) {
                this.logger.log("WARN", `To enable function inspection, please run "${process.execPath} is:npm i node@${backend.nodeMajorVersion} --save-dev" in your functions directory`);
            }
            else {
                const { host } = this.getInfo();
                args.unshift(`--inspect=${(0, utils_1.connectableHostname)(host)}:${this.args.debugPort}`);
            }
        }
        const pnpPath = path.join(backend.functionsDir, ".pnp.js");
        if (fs.existsSync(pnpPath)) {
            emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.FUNCTIONS).logLabeled("WARN_ONCE", "functions", "Detected yarn@2 with PnP. " +
                "Cloud Functions for Firebase requires a node_modules folder to work correctly and is therefore incompatible with PnP. " +
                "See https://yarnpkg.com/getting-started/migration#step-by-step for more information.");
        }
        const runtimeEnv = this.getRuntimeEnvs(backend, trigger);
        const secretEnvs = await this.resolveSecretEnvs(backend, trigger);
        const socketPath = (0, functionsEmulatorShared_1.getTemporarySocketPath)();
        const childProcess = spawn(backend.nodeBinary, args, {
            cwd: backend.functionsDir,
            env: Object.assign(Object.assign(Object.assign(Object.assign({ node: backend.nodeBinary }, process.env), runtimeEnv), secretEnvs), { PORT: socketPath }),
            stdio: ["pipe", "pipe", "pipe", "ipc"],
        });
        const runtime = {
            process: childProcess,
            events: emitter,
            cwd: backend.functionsDir,
            socketPath,
        };
        const extensionLogInfo = {
            instanceId: backend.extensionInstanceId,
            ref: (_a = backend.extensionVersion) === null || _a === void 0 ? void 0 : _a.ref,
        };
        const pool = this.workerPools[backend.codebase];
        const worker = pool.addWorker(trigger === null || trigger === void 0 ? void 0 : trigger.id, runtime, extensionLogInfo);
        await worker.waitForSocketReady();
        return worker;
    }
    async disableBackgroundTriggers() {
        Object.values(this.triggers).forEach((record) => {
            if (record.def.eventTrigger && record.enabled) {
                this.logger.logLabeled("BULLET", `functions[${record.def.entryPoint}]`, "function temporarily disabled.");
                record.enabled = false;
            }
        });
        await this.workQueue.flush();
    }
    async reloadTriggers() {
        this.triggerGeneration++;
        for (const backend of this.args.emulatableBackends) {
            await this.loadTriggers(backend);
        }
        await this.performPostLoadOperations();
        return;
    }
    getEmulatorInfo(emulator) {
        if (this.args.remoteEmulators) {
            if (this.args.remoteEmulators[emulator]) {
                return this.args.remoteEmulators[emulator];
            }
        }
        return registry_1.EmulatorRegistry.getInfo(emulator);
    }
    tokenFromAuthHeader(authHeader) {
        const match = /^Bearer (.*)$/.exec(authHeader);
        if (!match) {
            return;
        }
        let idToken = match[1];
        logger_1.logger.debug(`ID Token: ${idToken}`);
        if (idToken && idToken.includes("=")) {
            idToken = idToken.replace(/[=]+?\./g, ".");
            logger_1.logger.debug(`ID Token contained invalid padding, new value: ${idToken}`);
        }
        try {
            const decoded = jwt.decode(idToken, { complete: true });
            if (!decoded || typeof decoded !== "object") {
                logger_1.logger.debug(`Failed to decode ID Token: ${decoded}`);
                return;
            }
            const claims = decoded.payload;
            claims.uid = claims.sub;
            return claims;
        }
        catch (e) {
            return;
        }
    }
    async handleHttpsTrigger(req, res) {
        const method = req.method;
        let triggerId = req.params.trigger_name;
        if (req.params.region) {
            triggerId = `${req.params.region}-${triggerId}`;
        }
        if (!this.triggers[triggerId]) {
            res
                .status(404)
                .send(`Function ${triggerId} does not exist, valid functions are: ${Object.keys(this.triggers).join(", ")}`);
            return;
        }
        const record = this.getTriggerRecordByKey(triggerId);
        if (!record.enabled) {
            res.status(204).send("Background triggers are currently disabled.");
            return;
        }
        const trigger = record.def;
        logger_1.logger.debug(`Accepted request ${method} ${req.url} --> ${triggerId}`);
        const reqBody = req.rawBody;
        const isCallable = trigger.labels && trigger.labels["deployment-callable"] === "true";
        const authHeader = req.header("Authorization");
        if (authHeader && isCallable && trigger.platform !== "gcfv2") {
            const token = this.tokenFromAuthHeader(authHeader);
            if (token) {
                const contextAuth = {
                    uid: token.uid,
                    token: token,
                };
                req.headers[functionsEmulatorShared_1.HttpConstants.ORIGINAL_AUTH_HEADER] = req.headers["authorization"];
                delete req.headers["authorization"];
                req.headers[functionsEmulatorShared_1.HttpConstants.CALLABLE_AUTH_HEADER] = encodeURIComponent(JSON.stringify(contextAuth));
            }
        }
        void (0, track_1.track)(EVENT_INVOKE, (0, functionsEmulatorShared_1.getFunctionService)(trigger));
        void (0, track_1.trackEmulator)(EVENT_INVOKE_GA4, {
            function_service: (0, functionsEmulatorShared_1.getFunctionService)(trigger),
        });
        this.logger.log("DEBUG", `[functions] Runtime ready! Sending request!`);
        const url = new url_1.URL(`${req.protocol}://${req.hostname}${req.url}`);
        const path = `${url.pathname}${url.search}`.replace(new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), "/");
        this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`);
        const pool = this.workerPools[record.backend.codebase];
        if (!pool.readyForWork(trigger.id)) {
            await this.startRuntime(record.backend, trigger);
        }
        const debugBundle = this.args.debugPort
            ? {
                functionTarget: trigger.entryPoint,
                functionSignature: (0, functionsEmulatorShared_1.getSignatureType)(trigger),
            }
            : undefined;
        await pool.submitRequest(trigger.id, {
            method,
            path,
            headers: req.headers,
        }, res, reqBody, debugBundle);
    }
}
exports.FunctionsEmulator = FunctionsEmulator;
//# sourceMappingURL=functionsEmulator.js.map