123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945 |
- "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 {
- 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);
- }
- 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();
- }
- 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(triggerDefinitions.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;
|