UNPKG

firebase-tools

Version:
1,069 lines 55.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FunctionsEmulator = exports.TCPConn = exports.IPCConn = 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 semver = require("semver"); 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 portfinder = require("portfinder"); 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 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 python_1 = require("../functions/python"); const EVENT_INVOKE_GA4 = "functions_invoke"; const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$"); class IPCConn { constructor(socketPath) { this.socketPath = socketPath; } httpReqOpts() { return { socketPath: this.socketPath, }; } } exports.IPCConn = IPCConn; class TCPConn { constructor(host, port) { this.host = host; this.port = port; } httpReqOpts() { return { host: this.host, port: this.port, }; } } exports.TCPConn = TCPConn; 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 = {}; this.staticBackends = []; this.dynamicBackends = []; this.debugMode = false; this.staticBackends = args.emulatableBackends; emulatorLogger_1.EmulatorLogger.setVerbosity(this.args.verbosity ? emulatorLogger_1.Verbosity[this.args.verbosity] : emulatorLogger_1.Verbosity["DEBUG"]); if (this.args.debugPort) { const maybeNodeCodebases = this.staticBackends.filter((b) => !b.runtime || b.runtime.startsWith("node")); if (maybeNodeCodebases.length > 1 && typeof this.args.debugPort === "number") { throw new error_1.FirebaseError("Cannot debug on a single port with multiple codebases. " + "Use --inspect-functions=true to assign dynamic ports to each codebase"); } this.args.disabledRuntimeFeatures = this.args.disabledRuntimeFeatures || {}; this.args.disabledRuntimeFeatures.timeout = true; this.debugMode = true; } this.adminSdkConfig = Object.assign(Object.assign({}, this.args.adminSdkConfig), { projectId: this.args.projectId }); const mode = this.debugMode ? types_1.FunctionsExecutionMode.SEQUENTIAL : types_1.FunctionsExecutionMode.AUTO; this.workerPools = {}; for (const backend of this.staticBackends) { const pool = new functionsRuntimeWorker_1.RuntimeWorkerPool(mode); this.workerPools[backend.codebase] = pool; } this.workQueue = new workQueue_1.WorkQueue(mode); } async loadDynamicExtensionBackends() { if (this.args.extensionsEmulator) { const unfilteredBackends = this.args.extensionsEmulator.getDynamicExtensionBackends(); this.dynamicBackends = this.args.extensionsEmulator.filterUnemulatedTriggers(unfilteredBackends); const mode = this.debugMode ? types_1.FunctionsExecutionMode.SEQUENTIAL : types_1.FunctionsExecutionMode.AUTO; const credentialEnv = await (0, env_1.getCredentialsEnvironment)(this.args.account, this.logger, "functions"); for (const backend of this.dynamicBackends) { backend.env = Object.assign(Object.assign({}, credentialEnv), backend.env); if (this.workerPools[backend.codebase]) { if (this.debugMode) { this.workerPools[backend.codebase].exit(); } else { this.workerPools[backend.codebase].refresh(); } } else { const pool = new functionsRuntimeWorker_1.RuntimeWorkerPool(mode); this.workerPools[backend.codebase] = pool; } await this.loadTriggers(backend, true); } } } 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)) { try { await this.startRuntime(record.backend, trigger); } catch (e) { this.logger.logLabeled("ERROR", `Failed to start runtime for ${trigger.id}: ${e}`); return; } } const worker = pool.getIdleWorker(trigger.id); if (this.debugMode) { await worker.sendDebugMsg({ functionTarget: trigger.entryPoint, functionSignature: (0, functionsEmulatorShared_1.getSignatureType)(trigger), }); } const reqBody = JSON.stringify(body); const headers = { "Content-Type": "application/json", "Content-Length": `${reqBody.length}`, }; return new Promise((resolve, reject) => { const req = http.request(Object.assign(Object.assign({}, worker.runtime.conn.httpReqOpts()), { path: `/`, headers: headers }), resolve); req.on("error", reject); req.write(reqBody); req.end(); }); } async start() { const credentialEnv = await (0, env_1.getCredentialsEnvironment)(this.args.account, this.logger, "functions"); for (const e of this.staticBackends) { 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() { var _a, _b; for (const backend of this.staticBackends) { this.logger.logLabeled("BULLET", "functions", `Watching "${backend.functionsDir}" for Cloud Functions...`); const watcher = chokidar.watch(backend.functionsDir, { ignored: [ /.+?[\\\/]node_modules[\\\/].+?/, /(^|[\/\\])\../, /.+\.log/, /.+?[\\\/]venv[\\\/].+?/, ...((_b = (_a = backend.ignore) === null || _a === void 0 ? void 0 : _a.map((i) => `**/${i}`)) !== null && _b !== void 0 ? _b : []), ], 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, runtime: emulatableBackend.runtime, }; const runtimeDelegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext); logger_1.logger.debug(`Validating ${runtimeDelegate.language} source`); await runtimeDelegate.validate(); logger_1.logger.debug(`Building ${runtimeDelegate.language} source`); await runtimeDelegate.build(); emulatableBackend.runtime = runtimeDelegate.runtime; emulatableBackend.bin = runtimeDelegate.bin; 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, isEmulator: true, }; const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt); const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment); if (discoveredBuild.extensions && this.args.extensionsEmulator) { await this.args.extensionsEmulator.addDynamicExtensions(emulatableBackend.codebase, discoveredBuild); await this.loadDynamicExtensionBackends(); } const resolution = await (0, build_1.resolveBackend)({ build: discoveredBuild, firebaseConfig: JSON.parse(firebaseConfig), userEnvOpt, userEnvs, nonInteractive: false, isEmulator: true, }); 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) { var _a; 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; } if (this.debugMode) { this.workerPools[emulatableBackend.codebase].exit(); } else { this.workerPools[emulatableBackend.codebase].refresh(); } const toRemove = Object.keys(this.triggers).filter((recordKey) => { const record = this.getTriggerRecordByKey(recordKey); if (record.backend.codebase !== emulatableBackend.codebase) { return false; } if (force) { return true; } return !triggerDefinitions.some((def) => record.def.entryPoint === def.entryPoint && JSON.stringify(record.def.eventTrigger) === JSON.stringify(def.eventTrigger)); }); await this.removeTriggers(toRemove); 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); if (definition.taskQueueTrigger) { added = await this.addTaskQueueTrigger(this.args.projectId, definition.region, definition.name, url, definition.taskQueueTrigger); } } 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, signature); 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; case constants_1.Constants.SERVICE_FIREALERTS: added = await this.addFirealertsTrigger(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 triggerType = definition.httpsTrigger ? "http" : constants_1.Constants.getServiceName((0, functionsEmulatorShared_1.getFunctionService)(definition)); if (ignored) { const msg = `function ignored because the ${triggerType} emulator does not exist or is not running.`; this.logger.logLabeled("BULLET", `functions[${definition.id}]`, msg); } else { const msg = url ? `${clc.bold(triggerType)} function initialized (${url}).` : `${clc.bold(triggerType)} function initialized.`; this.logger.logLabeled("SUCCESS", `functions[${definition.id}]`, msg); } } if (this.debugMode) { if (!((_a = emulatableBackend.runtime) === null || _a === void 0 ? void 0 : _a.startsWith("node"))) { this.logger.log("WARN", "--inspect-functions only supported for Node.js runtimes."); } else { emulatableBackend.secretEnv = Object.values(triggerDefinitions.reduce((acc, curr) => { for (const secret of curr.secretEnvironmentVariables || []) { acc[secret.key] = secret; } return acc; }, {})); try { await this.startRuntime(emulatableBackend); } catch (e) { this.logger.logLabeled("ERROR", `Failed to start functions in ${emulatableBackend.functionsDir}: ${e}`); } } } } async removeTriggers(toRemove) { for (const triggerKey of toRemove) { const definition = this.triggers[triggerKey].def; const service = (0, functionsEmulatorShared_1.getFunctionService)(definition); const key = this.getTriggerKey(definition); switch (service) { case constants_1.Constants.SERVICE_EVENTARC: await this.removeEventarcTrigger(this.args.projectId, key, definition.eventTrigger); delete this.triggers[key]; break; case constants_1.Constants.SERVICE_FIREALERTS: await this.removeFirealertsTrigger(this.args.projectId, key, definition.eventTrigger); delete this.triggers[key]; break; default: break; } } } async addEventarcTrigger(projectId, key, eventTrigger) { if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.EVENTARC)) { return false; } const bundle = { eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "eventarc.googleapis.com" }), }; logger_1.logger.debug(`addEventarcTrigger`, JSON.stringify(bundle)); try { await registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC).post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle); return true; } catch (err) { this.logger.log("WARN", "Error adding Eventarc function: " + err); } return false; } async removeEventarcTrigger(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(`removeEventarcTrigger`, JSON.stringify(bundle)); try { await registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC).post(`/emulator/v1/remove/projects/${projectId}/triggers/${key}`, bundle); return true; } catch (err) { this.logger.log("WARN", "Error removing Eventarc function: " + err); } return false; } async addFirealertsTrigger(projectId, key, eventTrigger) { if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.EVENTARC)) { return false; } const bundle = { eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "firebasealerts.googleapis.com" }), }; logger_1.logger.debug(`addFirealertsTrigger`, JSON.stringify(bundle)); try { await registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC).post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle); return true; } catch (err) { this.logger.log("WARN", "Error adding FireAlerts function: " + err); } return false; } async removeFirealertsTrigger(projectId, key, eventTrigger) { if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.EVENTARC)) { return false; } const bundle = { eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "firebasealerts.googleapis.com" }), }; logger_1.logger.debug(`removeFirealertsTrigger`, JSON.stringify(bundle)); try { await registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC).post(`/emulator/v1/remove/projects/${projectId}/triggers/${key}`, bundle); return true; } catch (err) { this.logger.log("WARN", "Error removing FireAlerts 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; } getV1FirestoreAttributes(projectId, key, eventTrigger) { const bundle = JSON.stringify({ eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "firestore.googleapis.com" }), }); const path = `/emulator/v1/projects/${projectId}/triggers/${key}`; return { bundle, path }; } getV2FirestoreAttributes(projectId, key, eventTrigger) { var _a, _b, _c, _d, _e, _f; logger_1.logger.debug("Found a v2 firestore trigger."); const database = (_a = eventTrigger.eventFilters) === null || _a === void 0 ? void 0 : _a.database; if (!database) { throw new error_1.FirebaseError(`A database must be supplied for event trigger ${key}`); } const namespace = (_b = eventTrigger.eventFilters) === null || _b === void 0 ? void 0 : _b.namespace; if (!namespace) { throw new error_1.FirebaseError(`A namespace must be supplied for event trigger ${key}`); } let doc; let match; if ((_c = eventTrigger.eventFilters) === null || _c === void 0 ? void 0 : _c.document) { doc = (_d = eventTrigger.eventFilters) === null || _d === void 0 ? void 0 : _d.document; match = "EXACT"; } if ((_e = eventTrigger.eventFilterPathPatterns) === null || _e === void 0 ? void 0 : _e.document) { doc = (_f = eventTrigger.eventFilterPathPatterns) === null || _f === void 0 ? void 0 : _f.document; match = "PATH_PATTERN"; } if (!doc) { throw new error_1.FirebaseError("A document must be supplied."); } const bundle = JSON.stringify({ eventType: eventTrigger.eventType, database, namespace, document: { value: doc, matchType: match, }, }); const path = `/emulator/v1/projects/${projectId}/eventarcTrigger?eventarcTriggerId=${key}`; return { bundle, path }; } async addFirestoreTrigger(projectId, key, eventTrigger, signature) { if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.FIRESTORE)) { return Promise.resolve(false); } const { bundle, path } = signature === "cloudevent" ? this.getV2FirestoreAttributes(projectId, key, eventTrigger) : this.getV1FirestoreAttributes(projectId, key, eventTrigger); logger_1.logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle)); const client = registry_1.EmulatorRegistry.client(types_1.Emulators.FIRESTORE); try { signature === "cloudevent" ? await client.post(path, bundle) : await client.put(path, 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; } async addTaskQueueTrigger(projectId, location, entryPoint, defaultUri, taskQueueTrigger) { logger_1.logger.debug(`addTaskQueueTrigger`, JSON.stringify(taskQueueTrigger)); if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.TASKS)) { logger_1.logger.debug(`addTaskQueueTrigger`, "TQ not running"); return Promise.resolve(false); } const bundle = Object.assign(Object.assign({}, taskQueueTrigger), { defaultUri }); try { await registry_1.EmulatorRegistry.client(types_1.Emulators.TASKS).post(`/projects/${projectId}/locations/${location}/queues/${entryPoint}`, bundle); return true; } catch (err) { this.logger.log("WARN", "Error adding Task Queue function: " + err); return false; } } 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(); const backendInfo = this.staticBackends.map((e) => { return (0, functionsEmulatorShared_1.toBackendInfo)(e, cf3Triggers); }); const dynamicInfo = this.dynamicBackends.map((e) => { return (0, functionsEmulatorShared_1.toBackendInfo)(e, cf3Triggers, { createdBy: "SDK" }); }); backendInfo.push(...dynamicInfo); return backendInfo; } 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 })); } 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"; envs.GOOGLE_CLOUD_QUOTA_PROJECT = this.args.projectId; 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.debugMode) { 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 startNode(backend, envs) { const args = [path.join(__dirname, "functionsEmulatorRuntime")]; if (this.debugMode) { if (process.env.FIREPIT_VERSION) { this.logger.log("WARN", `To enable function inspection, please run "npm i node@${semver.coerce(backend.runtime || "18.0.0")} --save-dev" in your functions directory`); } else { let port; if (typeof this.args.debugPort === "number") { port = this.args.debugPort; } else { port = await portfinder.getPortPromise({ port: 9229 }); if (port === 9229) { this.logger.logLabeled("SUCCESS", "functions", `Using debug port 9229 for functions codebase ${backend.codebase}`); } else { this.logger.logLabeled("SUCCESS", "functions", `Using debug port ${port} for functions codebase ${backend.codebase}. ` + "You may need to add manually add this port to your inspector."); } } const { host } = this.getInfo(); args.unshift(`--inspect=${(0, utils_1.connectableHostname)(host)}:${port}`); } } 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 bin = backend.bin; if (!bin) { throw new Error(`No binary associated with ${backend.functionsDir}. ` + "Make sure function runtime is configured correctly in firebase.json."); } const socketPath = (0, functionsEmulatorShared_1.getTemporarySocketPath)(); const childProcess = spawn(bin, args, { cwd: backend.functionsDir, env: Object.assign(Object.assign(Object.assign({ node: backend.bin, METADATA_SERVER_DETECTION: "none" }, process.env), envs), { PORT: socketPath }), stdio: ["pipe", "pipe", "pipe", "ipc"], }); return Promise.resolve({ process: childProcess, events: new events_1.EventEmitter(), cwd: backend.functionsDir, conn: new IPCConn(socketPath), }); } async startPython(backend, envs) { const args = ["functions-framework"]; if (this.debugMode) { this.logger.log("WARN", "--inspect-functions not supported for Python functions. Ignored."); } const port = await portfinder.getPortPromise({ port: 8081 + (0, utils_1.randomInt)(0, 1000), }); const childProcess = (0, python_1.runWithVirtualEnv)(args, backend.functionsDir, Object.assign(Object.assign(Object.assign({}, process.env), envs), { PYTHONUNBUFFERED: "1", DEBUG: "False", HOST: "127.0.0.1", PORT: port.toString() })); return { process: childProcess, events: new events_1.EventEmitter(), cwd: backend.functionsDir, conn: new TCPConn("127.0.0.1", port), }; } async startRuntime(backend, trigger) { var _a; const runtimeEnv = this.getRuntimeEnvs(backend, trigger); const secretEnvs = await this.resolveSecretEnvs(backend, trigger); let runtime; if (backend.runtime.startsWith("python")) { runtime = await this.startPython(backend, Object.assign(Object.assign({}, runtimeEnv), secretEnvs)); } else { runtime = await this.startNode(backend, Object.assign(Object.assign({}, runtimeEnv), secretEnvs)); } 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, runtime, extensionLogInfo); await worker.waitForSocketReady(); return worker; } async disableBackgroundTriggers() { Object.values(this.triggers).forEach((record) => { if (record.def.eventTrigger && record.enabled) { this.logg