No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

functionsEmulator.js 44KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FunctionsEmulator = void 0;
  4. const fs = require("fs");
  5. const path = require("path");
  6. const express = require("express");
  7. const clc = require("colorette");
  8. const http = require("http");
  9. const jwt = require("jsonwebtoken");
  10. const cors = require("cors");
  11. const url_1 = require("url");
  12. const events_1 = require("events");
  13. const logger_1 = require("../logger");
  14. const track_1 = require("../track");
  15. const constants_1 = require("./constants");
  16. const types_1 = require("./types");
  17. const chokidar = require("chokidar");
  18. const spawn = require("cross-spawn");
  19. const functionsEmulatorShared_1 = require("./functionsEmulatorShared");
  20. const registry_1 = require("./registry");
  21. const emulatorLogger_1 = require("./emulatorLogger");
  22. const functionsRuntimeWorker_1 = require("./functionsRuntimeWorker");
  23. const error_1 = require("../error");
  24. const workQueue_1 = require("./workQueue");
  25. const utils_1 = require("../utils");
  26. const defaultCredentials_1 = require("../defaultCredentials");
  27. const adminSdkConfig_1 = require("./adminSdkConfig");
  28. const validate_1 = require("../deploy/functions/validate");
  29. const secretManager_1 = require("../gcp/secretManager");
  30. const runtimes = require("../deploy/functions/runtimes");
  31. const backend = require("../deploy/functions/backend");
  32. const functionsEnv = require("../functions/env");
  33. const v1_1 = require("../functions/events/v1");
  34. const build_1 = require("../deploy/functions/build");
  35. const env_1 = require("./env");
  36. const EVENT_INVOKE = "functions:invoke";
  37. const EVENT_INVOKE_GA4 = "functions_invoke";
  38. const DATABASE_PATH_PATTERN = new RegExp("^projects/[^/]+/instances/([^/]+)/refs(/.*)$");
  39. class FunctionsEmulator {
  40. constructor(args) {
  41. this.args = args;
  42. this.triggers = {};
  43. this.triggerGeneration = 0;
  44. this.logger = emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.FUNCTIONS);
  45. this.multicastTriggers = {};
  46. this.blockingFunctionsConfig = {};
  47. emulatorLogger_1.EmulatorLogger.verbosity = this.args.quiet ? emulatorLogger_1.Verbosity.QUIET : emulatorLogger_1.Verbosity.DEBUG;
  48. if (this.args.debugPort) {
  49. this.args.disabledRuntimeFeatures = this.args.disabledRuntimeFeatures || {};
  50. this.args.disabledRuntimeFeatures.timeout = true;
  51. }
  52. this.adminSdkConfig = Object.assign(Object.assign({}, this.args.adminSdkConfig), { projectId: this.args.projectId });
  53. const mode = this.args.debugPort
  54. ? types_1.FunctionsExecutionMode.SEQUENTIAL
  55. : types_1.FunctionsExecutionMode.AUTO;
  56. this.workerPools = {};
  57. for (const backend of this.args.emulatableBackends) {
  58. const pool = new functionsRuntimeWorker_1.RuntimeWorkerPool(mode);
  59. this.workerPools[backend.codebase] = pool;
  60. }
  61. this.workQueue = new workQueue_1.WorkQueue(mode);
  62. }
  63. static getHttpFunctionUrl(projectId, name, region, info) {
  64. let url;
  65. if (info) {
  66. url = new url_1.URL("http://" + (0, functionsEmulatorShared_1.formatHost)(info));
  67. }
  68. else {
  69. url = registry_1.EmulatorRegistry.url(types_1.Emulators.FUNCTIONS);
  70. }
  71. url.pathname = `/${projectId}/${region}/${name}`;
  72. return url.toString();
  73. }
  74. async getCredentialsEnvironment() {
  75. const credentialEnv = {};
  76. if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
  77. 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!`);
  78. }
  79. else if (this.args.account) {
  80. const defaultCredPath = await (0, defaultCredentials_1.getCredentialPathAsync)(this.args.account);
  81. if (defaultCredPath) {
  82. this.logger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
  83. credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
  84. }
  85. }
  86. else {
  87. 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.");
  88. }
  89. return credentialEnv;
  90. }
  91. createHubServer() {
  92. this.workQueue.start();
  93. const hub = express();
  94. const dataMiddleware = (req, res, next) => {
  95. const chunks = [];
  96. req.on("data", (chunk) => {
  97. chunks.push(chunk);
  98. });
  99. req.on("end", () => {
  100. req.rawBody = Buffer.concat(chunks);
  101. next();
  102. });
  103. };
  104. const backgroundFunctionRoute = `/functions/projects/:project_id/triggers/:trigger_name(*)`;
  105. const httpsFunctionRoute = `/${this.args.projectId}/:region/:trigger_name`;
  106. const multicastFunctionRoute = `/functions/projects/:project_id/trigger_multicast`;
  107. const httpsFunctionRoutes = [httpsFunctionRoute, `${httpsFunctionRoute}/*`];
  108. const listBackendsRoute = `/backends`;
  109. const httpsHandler = (req, res) => {
  110. const work = () => {
  111. return this.handleHttpsTrigger(req, res);
  112. };
  113. work.type = `${req.path}-${new Date().toISOString()}`;
  114. this.workQueue.submit(work);
  115. };
  116. const multicastHandler = (req, res) => {
  117. var _a;
  118. const projectId = req.params.project_id;
  119. const rawBody = req.rawBody;
  120. const event = JSON.parse(rawBody.toString());
  121. let triggerKey;
  122. if ((_a = req.headers["content-type"]) === null || _a === void 0 ? void 0 : _a.includes("cloudevent")) {
  123. triggerKey = `${this.args.projectId}:${event.type}`;
  124. }
  125. else {
  126. triggerKey = `${this.args.projectId}:${event.eventType}`;
  127. }
  128. if (event.data.bucket) {
  129. triggerKey += `:${event.data.bucket}`;
  130. }
  131. const triggers = this.multicastTriggers[triggerKey] || [];
  132. const { host, port } = this.getInfo();
  133. triggers.forEach((triggerId) => {
  134. const work = () => {
  135. return new Promise((resolve, reject) => {
  136. const trigReq = http.request({
  137. host: (0, utils_1.connectableHostname)(host),
  138. port,
  139. method: req.method,
  140. path: `/functions/projects/${projectId}/triggers/${triggerId}`,
  141. headers: req.headers,
  142. });
  143. trigReq.on("error", reject);
  144. trigReq.write(rawBody);
  145. trigReq.end();
  146. resolve();
  147. });
  148. };
  149. work.type = `${triggerId}-${new Date().toISOString()}`;
  150. this.workQueue.submit(work);
  151. });
  152. res.json({ status: "multicast_acknowledged" });
  153. };
  154. const listBackendsHandler = (req, res) => {
  155. res.json({ backends: this.getBackendInfo() });
  156. };
  157. hub.get(listBackendsRoute, cors({ origin: true }), listBackendsHandler);
  158. hub.post(backgroundFunctionRoute, dataMiddleware, httpsHandler);
  159. hub.post(multicastFunctionRoute, dataMiddleware, multicastHandler);
  160. hub.all(httpsFunctionRoutes, dataMiddleware, httpsHandler);
  161. hub.all("*", dataMiddleware, (req, res) => {
  162. logger_1.logger.debug(`Functions emulator received unknown request at path ${req.path}`);
  163. res.sendStatus(404);
  164. });
  165. return hub;
  166. }
  167. async sendRequest(trigger, body) {
  168. const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
  169. const pool = this.workerPools[record.backend.codebase];
  170. if (!pool.readyForWork(trigger.id)) {
  171. await this.startRuntime(record.backend, trigger);
  172. }
  173. const worker = pool.getIdleWorker(trigger.id);
  174. const reqBody = JSON.stringify(body);
  175. const headers = {
  176. "Content-Type": "application/json",
  177. "Content-Length": `${reqBody.length}`,
  178. };
  179. return new Promise((resolve, reject) => {
  180. const req = http.request({
  181. path: `/`,
  182. socketPath: worker.runtime.socketPath,
  183. headers: headers,
  184. }, resolve);
  185. req.on("error", reject);
  186. req.write(reqBody);
  187. req.end();
  188. });
  189. }
  190. async start() {
  191. for (const backend of this.args.emulatableBackends) {
  192. backend.nodeBinary = this.getNodeBinary(backend);
  193. }
  194. const credentialEnv = await this.getCredentialsEnvironment();
  195. for (const e of this.args.emulatableBackends) {
  196. e.env = Object.assign(Object.assign({}, credentialEnv), e.env);
  197. }
  198. if (Object.keys(this.adminSdkConfig || {}).length <= 1) {
  199. const adminSdkConfig = await (0, adminSdkConfig_1.getProjectAdminSdkConfigOrCached)(this.args.projectId);
  200. if (adminSdkConfig) {
  201. this.adminSdkConfig = adminSdkConfig;
  202. }
  203. else {
  204. this.logger.logLabeled("WARN", "functions", "Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect.");
  205. this.adminSdkConfig = (0, adminSdkConfig_1.constructDefaultAdminSdkConfig)(this.args.projectId);
  206. }
  207. }
  208. const { host, port } = this.getInfo();
  209. this.workQueue.start();
  210. const server = this.createHubServer().listen(port, host);
  211. this.destroyServer = (0, utils_1.createDestroyer)(server);
  212. return Promise.resolve();
  213. }
  214. async connect() {
  215. for (const backend of this.args.emulatableBackends) {
  216. this.logger.logLabeled("BULLET", "functions", `Watching "${backend.functionsDir}" for Cloud Functions...`);
  217. const watcher = chokidar.watch(backend.functionsDir, {
  218. ignored: [
  219. /.+?[\\\/]node_modules[\\\/].+?/,
  220. /(^|[\/\\])\../,
  221. /.+\.log/,
  222. ],
  223. persistent: true,
  224. });
  225. const debouncedLoadTriggers = (0, utils_1.debounce)(() => this.loadTriggers(backend), 1000);
  226. watcher.on("change", (filePath) => {
  227. this.logger.log("DEBUG", `File ${filePath} changed, reloading triggers`);
  228. return debouncedLoadTriggers();
  229. });
  230. await this.loadTriggers(backend, true);
  231. }
  232. await this.performPostLoadOperations();
  233. return;
  234. }
  235. async stop() {
  236. try {
  237. await this.workQueue.flush();
  238. }
  239. catch (e) {
  240. this.logger.logLabeled("WARN", "functions", "Functions emulator work queue did not empty before stopping");
  241. }
  242. this.workQueue.stop();
  243. for (const pool of Object.values(this.workerPools)) {
  244. pool.exit();
  245. }
  246. if (this.destroyServer) {
  247. await this.destroyServer();
  248. }
  249. }
  250. async discoverTriggers(emulatableBackend) {
  251. if (emulatableBackend.predefinedTriggers) {
  252. return (0, functionsEmulatorShared_1.emulatedFunctionsByRegion)(emulatableBackend.predefinedTriggers, emulatableBackend.secretEnv);
  253. }
  254. else {
  255. const runtimeConfig = this.getRuntimeConfig(emulatableBackend);
  256. const runtimeDelegateContext = {
  257. projectId: this.args.projectId,
  258. projectDir: this.args.projectDir,
  259. sourceDir: emulatableBackend.functionsDir,
  260. };
  261. if (emulatableBackend.nodeMajorVersion) {
  262. runtimeDelegateContext.runtime = `nodejs${emulatableBackend.nodeMajorVersion}`;
  263. }
  264. const runtimeDelegate = await runtimes.getRuntimeDelegate(runtimeDelegateContext);
  265. logger_1.logger.debug(`Validating ${runtimeDelegate.name} source`);
  266. await runtimeDelegate.validate();
  267. logger_1.logger.debug(`Building ${runtimeDelegate.name} source`);
  268. await runtimeDelegate.build();
  269. const firebaseConfig = this.getFirebaseConfig();
  270. const environment = Object.assign(Object.assign(Object.assign(Object.assign({}, this.getSystemEnvs()), this.getEmulatorEnvs()), { FIREBASE_CONFIG: firebaseConfig }), emulatableBackend.env);
  271. const userEnvOpt = {
  272. functionsSource: emulatableBackend.functionsDir,
  273. projectId: this.args.projectId,
  274. projectAlias: this.args.projectAlias,
  275. };
  276. const userEnvs = functionsEnv.loadUserEnvs(userEnvOpt);
  277. const discoveredBuild = await runtimeDelegate.discoverBuild(runtimeConfig, environment);
  278. const resolution = await (0, build_1.resolveBackend)(discoveredBuild, JSON.parse(firebaseConfig), userEnvOpt, userEnvs);
  279. const discoveredBackend = resolution.backend;
  280. const endpoints = backend.allEndpoints(discoveredBackend);
  281. (0, functionsEmulatorShared_1.prepareEndpoints)(endpoints);
  282. for (const e of endpoints) {
  283. e.codebase = emulatableBackend.codebase;
  284. }
  285. return (0, functionsEmulatorShared_1.emulatedFunctionsFromEndpoints)(endpoints);
  286. }
  287. }
  288. async loadTriggers(emulatableBackend, force = false) {
  289. if (!emulatableBackend.nodeBinary) {
  290. throw new error_1.FirebaseError(`No node binary for ${emulatableBackend.functionsDir}. This should never happen.`);
  291. }
  292. let triggerDefinitions = [];
  293. try {
  294. triggerDefinitions = await this.discoverTriggers(emulatableBackend);
  295. this.logger.logLabeled("SUCCESS", "functions", `Loaded functions definitions from source: ${triggerDefinitions
  296. .map((t) => t.entryPoint)
  297. .join(", ")}.`);
  298. }
  299. catch (e) {
  300. this.logger.logLabeled("ERROR", "functions", `Failed to load function definition from source: ${e}`);
  301. return;
  302. }
  303. this.workerPools[emulatableBackend.codebase].refresh();
  304. this.blockingFunctionsConfig = {};
  305. const toSetup = triggerDefinitions.filter((definition) => {
  306. if (force) {
  307. return true;
  308. }
  309. const anyEnabledMatch = Object.values(this.triggers).some((record) => {
  310. const sameEntryPoint = record.def.entryPoint === definition.entryPoint;
  311. const sameEventTrigger = JSON.stringify(record.def.eventTrigger) === JSON.stringify(definition.eventTrigger);
  312. if (sameEntryPoint && !sameEventTrigger) {
  313. this.logger.log("DEBUG", `Definition for trigger ${definition.entryPoint} changed from ${JSON.stringify(record.def.eventTrigger)} to ${JSON.stringify(definition.eventTrigger)}`);
  314. }
  315. return record.enabled && sameEntryPoint && sameEventTrigger;
  316. });
  317. return !anyEnabledMatch;
  318. });
  319. for (const definition of toSetup) {
  320. try {
  321. (0, validate_1.functionIdsAreValid)([Object.assign(Object.assign({}, definition), { id: definition.name })]);
  322. }
  323. catch (e) {
  324. throw new error_1.FirebaseError(`functions[${definition.id}]: Invalid function id: ${e.message}`);
  325. }
  326. let added = false;
  327. let url = undefined;
  328. if (definition.httpsTrigger) {
  329. added = true;
  330. url = FunctionsEmulator.getHttpFunctionUrl(this.args.projectId, definition.name, definition.region);
  331. }
  332. else if (definition.eventTrigger) {
  333. const service = (0, functionsEmulatorShared_1.getFunctionService)(definition);
  334. const key = this.getTriggerKey(definition);
  335. const signature = (0, functionsEmulatorShared_1.getSignatureType)(definition);
  336. switch (service) {
  337. case constants_1.Constants.SERVICE_FIRESTORE:
  338. added = await this.addFirestoreTrigger(this.args.projectId, key, definition.eventTrigger);
  339. break;
  340. case constants_1.Constants.SERVICE_REALTIME_DATABASE:
  341. added = await this.addRealtimeDatabaseTrigger(this.args.projectId, definition.id, key, definition.eventTrigger, signature, definition.region);
  342. break;
  343. case constants_1.Constants.SERVICE_PUBSUB:
  344. added = await this.addPubsubTrigger(definition.name, key, definition.eventTrigger, signature, definition.schedule);
  345. break;
  346. case constants_1.Constants.SERVICE_EVENTARC:
  347. added = await this.addEventarcTrigger(this.args.projectId, key, definition.eventTrigger);
  348. break;
  349. case constants_1.Constants.SERVICE_AUTH:
  350. added = this.addAuthTrigger(this.args.projectId, key, definition.eventTrigger);
  351. break;
  352. case constants_1.Constants.SERVICE_STORAGE:
  353. added = this.addStorageTrigger(this.args.projectId, key, definition.eventTrigger);
  354. break;
  355. default:
  356. this.logger.log("DEBUG", `Unsupported trigger: ${JSON.stringify(definition)}`);
  357. break;
  358. }
  359. }
  360. else if (definition.blockingTrigger) {
  361. url = FunctionsEmulator.getHttpFunctionUrl(this.args.projectId, definition.name, definition.region);
  362. added = this.addBlockingTrigger(url, definition.blockingTrigger);
  363. }
  364. else {
  365. this.logger.log("WARN", `Unsupported function type on ${definition.name}. Expected either an httpsTrigger, eventTrigger, or blockingTrigger.`);
  366. }
  367. const ignored = !added;
  368. this.addTriggerRecord(definition, { backend: emulatableBackend, ignored, url });
  369. const type = definition.httpsTrigger
  370. ? "http"
  371. : constants_1.Constants.getServiceName((0, functionsEmulatorShared_1.getFunctionService)(definition));
  372. if (ignored) {
  373. const msg = `function ignored because the ${type} emulator does not exist or is not running.`;
  374. this.logger.logLabeled("BULLET", `functions[${definition.id}]`, msg);
  375. }
  376. else {
  377. const msg = url
  378. ? `${clc.bold(type)} function initialized (${url}).`
  379. : `${clc.bold(type)} function initialized.`;
  380. this.logger.logLabeled("SUCCESS", `functions[${definition.id}]`, msg);
  381. }
  382. }
  383. if (this.args.debugPort) {
  384. emulatableBackend.secretEnv = Object.values(triggerDefinitions.reduce((acc, curr) => {
  385. for (const secret of curr.secretEnvironmentVariables || []) {
  386. acc[secret.key] = secret;
  387. }
  388. return acc;
  389. }, {}));
  390. await this.startRuntime(emulatableBackend);
  391. }
  392. }
  393. addEventarcTrigger(projectId, key, eventTrigger) {
  394. if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.EVENTARC)) {
  395. return Promise.resolve(false);
  396. }
  397. const bundle = {
  398. eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "eventarc.googleapis.com" }),
  399. };
  400. logger_1.logger.debug(`addEventarcTrigger`, JSON.stringify(bundle));
  401. return registry_1.EmulatorRegistry.client(types_1.Emulators.EVENTARC)
  402. .post(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle)
  403. .then(() => true)
  404. .catch((err) => {
  405. this.logger.log("WARN", "Error adding Eventarc function: " + err);
  406. return false;
  407. });
  408. }
  409. async performPostLoadOperations() {
  410. if (!this.blockingFunctionsConfig.triggers &&
  411. !this.blockingFunctionsConfig.forwardInboundCredentials) {
  412. return;
  413. }
  414. if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.AUTH)) {
  415. return;
  416. }
  417. const path = `/identitytoolkit.googleapis.com/v2/projects/${this.getProjectId()}/config?updateMask=blockingFunctions`;
  418. try {
  419. const client = registry_1.EmulatorRegistry.client(types_1.Emulators.AUTH);
  420. await client.patch(path, { blockingFunctions: this.blockingFunctionsConfig }, {
  421. headers: { Authorization: "Bearer owner" },
  422. });
  423. }
  424. catch (err) {
  425. this.logger.log("WARN", "Error updating blocking functions config to the auth emulator: " + err);
  426. throw err;
  427. }
  428. }
  429. getV1DatabaseApiAttributes(projectId, key, eventTrigger) {
  430. const result = DATABASE_PATH_PATTERN.exec(eventTrigger.resource);
  431. if (result === null || result.length !== 3) {
  432. this.logger.log("WARN", `Event function "${key}" has malformed "resource" member. ` + `${eventTrigger.resource}`);
  433. throw new error_1.FirebaseError(`Event function ${key} has malformed resource member`);
  434. }
  435. const instance = result[1];
  436. const bundle = JSON.stringify({
  437. name: `projects/${projectId}/locations/_/functions/${key}`,
  438. path: result[2],
  439. event: eventTrigger.eventType,
  440. topic: `projects/${projectId}/topics/${key}`,
  441. });
  442. let apiPath = "/.settings/functionTriggers.json";
  443. if (instance !== "") {
  444. apiPath += `?ns=${instance}`;
  445. }
  446. else {
  447. this.logger.log("WARN", `No project in use. Registering function for sentinel namespace '${constants_1.Constants.DEFAULT_DATABASE_EMULATOR_NAMESPACE}'`);
  448. }
  449. return { bundle, apiPath, instance };
  450. }
  451. getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region) {
  452. var _a, _b, _c;
  453. const instance = ((_a = eventTrigger.eventFilters) === null || _a === void 0 ? void 0 : _a.instance) || ((_b = eventTrigger.eventFilterPathPatterns) === null || _b === void 0 ? void 0 : _b.instance);
  454. if (!instance) {
  455. throw new error_1.FirebaseError("A database instance must be supplied.");
  456. }
  457. const ref = (_c = eventTrigger.eventFilterPathPatterns) === null || _c === void 0 ? void 0 : _c.ref;
  458. if (!ref) {
  459. throw new error_1.FirebaseError("A database reference must be supplied.");
  460. }
  461. if (region !== "us-central1") {
  462. this.logger.logLabeled("WARN", `functions[${id}]`, `function region is defined outside the database region, will not trigger.`);
  463. }
  464. const bundle = JSON.stringify({
  465. name: `projects/${projectId}/locations/${region}/triggers/${key}`,
  466. path: ref,
  467. event: eventTrigger.eventType,
  468. topic: `projects/${projectId}/topics/${key}`,
  469. namespacePattern: instance,
  470. });
  471. const apiPath = "/.settings/functionTriggers.json";
  472. return { bundle, apiPath, instance };
  473. }
  474. async addRealtimeDatabaseTrigger(projectId, id, key, eventTrigger, signature, region) {
  475. if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.DATABASE)) {
  476. return false;
  477. }
  478. const { bundle, apiPath, instance } = signature === "cloudevent"
  479. ? this.getV2DatabaseApiAttributes(projectId, id, key, eventTrigger, region)
  480. : this.getV1DatabaseApiAttributes(projectId, key, eventTrigger);
  481. logger_1.logger.debug(`addRealtimeDatabaseTrigger[${instance}]`, JSON.stringify(bundle));
  482. const client = registry_1.EmulatorRegistry.client(types_1.Emulators.DATABASE);
  483. try {
  484. await client.post(apiPath, bundle, { headers: { Authorization: "Bearer owner" } });
  485. }
  486. catch (err) {
  487. this.logger.log("WARN", "Error adding Realtime Database function: " + err);
  488. throw err;
  489. }
  490. return true;
  491. }
  492. async addFirestoreTrigger(projectId, key, eventTrigger) {
  493. if (!registry_1.EmulatorRegistry.isRunning(types_1.Emulators.FIRESTORE)) {
  494. return Promise.resolve(false);
  495. }
  496. const bundle = JSON.stringify({
  497. eventTrigger: Object.assign(Object.assign({}, eventTrigger), { service: "firestore.googleapis.com" }),
  498. });
  499. logger_1.logger.debug(`addFirestoreTrigger`, JSON.stringify(bundle));
  500. const client = registry_1.EmulatorRegistry.client(types_1.Emulators.FIRESTORE);
  501. try {
  502. await client.put(`/emulator/v1/projects/${projectId}/triggers/${key}`, bundle);
  503. }
  504. catch (err) {
  505. this.logger.log("WARN", "Error adding firestore function: " + err);
  506. throw err;
  507. }
  508. return true;
  509. }
  510. async addPubsubTrigger(triggerName, key, eventTrigger, signatureType, schedule) {
  511. const pubsubEmulator = registry_1.EmulatorRegistry.get(types_1.Emulators.PUBSUB);
  512. if (!pubsubEmulator) {
  513. return false;
  514. }
  515. logger_1.logger.debug(`addPubsubTrigger`, JSON.stringify({ eventTrigger }));
  516. const resource = eventTrigger.resource;
  517. let topic;
  518. if (schedule) {
  519. topic = "firebase-schedule-" + triggerName;
  520. }
  521. else {
  522. const resourceParts = resource.split("/");
  523. topic = resourceParts[resourceParts.length - 1];
  524. }
  525. try {
  526. await pubsubEmulator.addTrigger(topic, key, signatureType);
  527. return true;
  528. }
  529. catch (e) {
  530. return false;
  531. }
  532. }
  533. addAuthTrigger(projectId, key, eventTrigger) {
  534. logger_1.logger.debug(`addAuthTrigger`, JSON.stringify({ eventTrigger }));
  535. const eventTriggerId = `${projectId}:${eventTrigger.eventType}`;
  536. const triggers = this.multicastTriggers[eventTriggerId] || [];
  537. triggers.push(key);
  538. this.multicastTriggers[eventTriggerId] = triggers;
  539. return true;
  540. }
  541. addStorageTrigger(projectId, key, eventTrigger) {
  542. logger_1.logger.debug(`addStorageTrigger`, JSON.stringify({ eventTrigger }));
  543. const bucket = eventTrigger.resource.startsWith("projects/_/buckets/")
  544. ? eventTrigger.resource.split("/")[3]
  545. : eventTrigger.resource;
  546. const eventTriggerId = `${projectId}:${eventTrigger.eventType}:${bucket}`;
  547. const triggers = this.multicastTriggers[eventTriggerId] || [];
  548. triggers.push(key);
  549. this.multicastTriggers[eventTriggerId] = triggers;
  550. return true;
  551. }
  552. addBlockingTrigger(url, blockingTrigger) {
  553. logger_1.logger.debug(`addBlockingTrigger`, JSON.stringify({ blockingTrigger }));
  554. const eventType = blockingTrigger.eventType;
  555. if (!v1_1.AUTH_BLOCKING_EVENTS.includes(eventType)) {
  556. return false;
  557. }
  558. if (blockingTrigger.eventType === v1_1.BEFORE_CREATE_EVENT) {
  559. this.blockingFunctionsConfig.triggers = Object.assign(Object.assign({}, this.blockingFunctionsConfig.triggers), { beforeCreate: {
  560. functionUri: url,
  561. } });
  562. }
  563. else {
  564. this.blockingFunctionsConfig.triggers = Object.assign(Object.assign({}, this.blockingFunctionsConfig.triggers), { beforeSignIn: {
  565. functionUri: url,
  566. } });
  567. }
  568. this.blockingFunctionsConfig.forwardInboundCredentials = {
  569. accessToken: !!blockingTrigger.options.accessToken,
  570. idToken: !!blockingTrigger.options.idToken,
  571. refreshToken: !!blockingTrigger.options.refreshToken,
  572. };
  573. return true;
  574. }
  575. getProjectId() {
  576. return this.args.projectId;
  577. }
  578. getInfo() {
  579. const host = this.args.host || constants_1.Constants.getDefaultHost();
  580. const port = this.args.port || constants_1.Constants.getDefaultPort(types_1.Emulators.FUNCTIONS);
  581. return {
  582. name: this.getName(),
  583. host,
  584. port,
  585. };
  586. }
  587. getName() {
  588. return types_1.Emulators.FUNCTIONS;
  589. }
  590. getTriggerDefinitions() {
  591. return Object.values(this.triggers).map((record) => record.def);
  592. }
  593. getTriggerRecordByKey(triggerKey) {
  594. const record = this.triggers[triggerKey];
  595. if (!record) {
  596. logger_1.logger.debug(`Could not find key=${triggerKey} in ${JSON.stringify(this.triggers)}`);
  597. throw new error_1.FirebaseError(`No function with key ${triggerKey}`);
  598. }
  599. return record;
  600. }
  601. getTriggerKey(def) {
  602. if (def.eventTrigger) {
  603. const triggerKey = `${def.id}-${this.triggerGeneration}`;
  604. return def.eventTrigger.channel ? `${triggerKey}-${def.eventTrigger.channel}` : triggerKey;
  605. }
  606. else {
  607. return def.id;
  608. }
  609. }
  610. getBackendInfo() {
  611. const cf3Triggers = this.getCF3Triggers();
  612. return this.args.emulatableBackends.map((e) => {
  613. return (0, functionsEmulatorShared_1.toBackendInfo)(e, cf3Triggers);
  614. });
  615. }
  616. getCF3Triggers() {
  617. return Object.values(this.triggers)
  618. .filter((t) => !t.backend.extensionInstanceId)
  619. .map((t) => t.def);
  620. }
  621. addTriggerRecord(def, opts) {
  622. const key = this.getTriggerKey(def);
  623. this.triggers[key] = {
  624. def,
  625. enabled: true,
  626. backend: opts.backend,
  627. ignored: opts.ignored,
  628. url: opts.url,
  629. };
  630. }
  631. setTriggersForTesting(triggers, backend) {
  632. this.triggers = {};
  633. triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false }));
  634. }
  635. getNodeBinary(backend) {
  636. const pkg = require(path.join(backend.functionsDir, "package.json"));
  637. if ((!pkg.engines || !pkg.engines.node) && !backend.nodeMajorVersion) {
  638. this.logger.log("WARN", `Your functions directory ${backend.functionsDir} does not specify a Node version.\n ` +
  639. "- Learn more at https://firebase.google.com/docs/functions/manage-functions#set_runtime_options");
  640. return process.execPath;
  641. }
  642. const hostMajorVersion = process.versions.node.split(".")[0];
  643. const requestedMajorVersion = backend.nodeMajorVersion
  644. ? `${backend.nodeMajorVersion}`
  645. : pkg.engines.node;
  646. let localMajorVersion = "0";
  647. const localNodePath = path.join(backend.functionsDir, "node_modules/.bin/node");
  648. try {
  649. const localNodeOutput = spawn.sync(localNodePath, ["--version"]).stdout.toString();
  650. localMajorVersion = localNodeOutput.slice(1).split(".")[0];
  651. }
  652. catch (err) {
  653. }
  654. if (requestedMajorVersion === localMajorVersion) {
  655. this.logger.logLabeled("SUCCESS", "functions", `Using node@${requestedMajorVersion} from local cache.`);
  656. return localNodePath;
  657. }
  658. if (requestedMajorVersion === hostMajorVersion) {
  659. this.logger.logLabeled("SUCCESS", "functions", `Using node@${requestedMajorVersion} from host.`);
  660. }
  661. else {
  662. if (process.env.FIREPIT_VERSION) {
  663. this.logger.log("WARN", `You've requested "node" version "${requestedMajorVersion}", but the standalone Firebase CLI comes with bundled Node "${hostMajorVersion}".`);
  664. this.logger.log("INFO", `To use a different Node.js version, consider removing the standalone Firebase CLI and switching to "firebase-tools" on npm.`);
  665. }
  666. else {
  667. this.logger.log("WARN", `Your requested "node" version "${requestedMajorVersion}" doesn't match your global version "${hostMajorVersion}". Using node@${hostMajorVersion} from host.`);
  668. }
  669. }
  670. return process.execPath;
  671. }
  672. getRuntimeConfig(backend) {
  673. const configPath = `${backend.functionsDir}/.runtimeconfig.json`;
  674. try {
  675. const configContent = fs.readFileSync(configPath, "utf8");
  676. return JSON.parse(configContent.toString());
  677. }
  678. catch (e) {
  679. }
  680. return {};
  681. }
  682. getUserEnvs(backend) {
  683. const projectInfo = {
  684. functionsSource: backend.functionsDir,
  685. projectId: this.args.projectId,
  686. projectAlias: this.args.projectAlias,
  687. isEmulator: true,
  688. };
  689. if (functionsEnv.hasUserEnvs(projectInfo)) {
  690. try {
  691. return functionsEnv.loadUserEnvs(projectInfo);
  692. }
  693. catch (e) {
  694. logger_1.logger.debug("Failed to load local environment variables", e);
  695. }
  696. }
  697. return {};
  698. }
  699. getSystemEnvs(trigger) {
  700. const envs = {};
  701. envs.GCLOUD_PROJECT = this.args.projectId;
  702. envs.K_REVISION = "1";
  703. envs.PORT = "80";
  704. if (trigger === null || trigger === void 0 ? void 0 : trigger.timeoutSeconds) {
  705. envs.FUNCTIONS_EMULATOR_TIMEOUT_SECONDS = trigger.timeoutSeconds.toString();
  706. }
  707. if (trigger) {
  708. const target = trigger.entryPoint;
  709. envs.FUNCTION_TARGET = target;
  710. envs.FUNCTION_SIGNATURE_TYPE = (0, functionsEmulatorShared_1.getSignatureType)(trigger);
  711. envs.K_SERVICE = trigger.name;
  712. }
  713. return envs;
  714. }
  715. getEmulatorEnvs() {
  716. const envs = {};
  717. envs.FUNCTIONS_EMULATOR = "true";
  718. envs.TZ = "UTC";
  719. envs.FIREBASE_DEBUG_MODE = "true";
  720. envs.FIREBASE_DEBUG_FEATURES = JSON.stringify({
  721. skipTokenVerification: true,
  722. enableCors: true,
  723. });
  724. let emulatorInfos = registry_1.EmulatorRegistry.listRunningWithInfo();
  725. if (this.args.remoteEmulators) {
  726. emulatorInfos = emulatorInfos.concat(Object.values(this.args.remoteEmulators));
  727. }
  728. (0, env_1.setEnvVarsForEmulators)(envs, emulatorInfos);
  729. if (this.args.debugPort) {
  730. envs["FUNCTION_DEBUG_MODE"] = "true";
  731. }
  732. return envs;
  733. }
  734. getFirebaseConfig() {
  735. const databaseEmulator = this.getEmulatorInfo(types_1.Emulators.DATABASE);
  736. let emulatedDatabaseURL = undefined;
  737. if (databaseEmulator) {
  738. let ns = this.args.projectId;
  739. if (this.adminSdkConfig.databaseURL) {
  740. const asUrl = new url_1.URL(this.adminSdkConfig.databaseURL);
  741. ns = asUrl.hostname.split(".")[0];
  742. }
  743. emulatedDatabaseURL = `http://${(0, functionsEmulatorShared_1.formatHost)(databaseEmulator)}/?ns=${ns}`;
  744. }
  745. return JSON.stringify({
  746. storageBucket: this.adminSdkConfig.storageBucket,
  747. databaseURL: emulatedDatabaseURL || this.adminSdkConfig.databaseURL,
  748. projectId: this.args.projectId,
  749. });
  750. }
  751. getRuntimeEnvs(backend, trigger) {
  752. 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);
  753. }
  754. async resolveSecretEnvs(backend, trigger) {
  755. let secretEnvs = {};
  756. const secretPath = (0, functionsEmulatorShared_1.getSecretLocalPath)(backend, this.args.projectDir);
  757. try {
  758. const data = fs.readFileSync(secretPath, "utf8");
  759. secretEnvs = functionsEnv.parseStrict(data);
  760. }
  761. catch (e) {
  762. if (e.code !== "ENOENT") {
  763. this.logger.logLabeled("ERROR", "functions", `Failed to read local secrets file ${secretPath}: ${e.message}`);
  764. }
  765. }
  766. const secrets = (trigger === null || trigger === void 0 ? void 0 : trigger.secretEnvironmentVariables) || backend.secretEnv;
  767. const accesses = secrets
  768. .filter((s) => !secretEnvs[s.key])
  769. .map(async (s) => {
  770. var _a;
  771. this.logger.logLabeled("INFO", "functions", `Trying to access secret ${s.secret}@latest`);
  772. const value = await (0, secretManager_1.accessSecretVersion)(this.getProjectId(), s.secret, (_a = s.version) !== null && _a !== void 0 ? _a : "latest");
  773. return [s.key, value];
  774. });
  775. const accessResults = await (0, utils_1.allSettled)(accesses);
  776. const errs = [];
  777. for (const result of accessResults) {
  778. if (result.status === "rejected") {
  779. errs.push(result.reason);
  780. }
  781. else {
  782. const [k, v] = result.value;
  783. secretEnvs[k] = v;
  784. }
  785. }
  786. if (errs.length > 0) {
  787. this.logger.logLabeled("ERROR", "functions", "Unable to access secret environment variables from Google Cloud Secret Manager. " +
  788. "Make sure the credential used for the Functions Emulator have access " +
  789. `or provide override values in ${secretPath}:\n\t` +
  790. errs.join("\n\t"));
  791. }
  792. return secretEnvs;
  793. }
  794. async startRuntime(backend, trigger) {
  795. var _a;
  796. const emitter = new events_1.EventEmitter();
  797. const args = [path.join(__dirname, "functionsEmulatorRuntime")];
  798. if (this.args.debugPort) {
  799. if (process.env.FIREPIT_VERSION && process.execPath === backend.nodeBinary) {
  800. this.logger.log("WARN", `To enable function inspection, please run "${process.execPath} is:npm i node@${backend.nodeMajorVersion} --save-dev" in your functions directory`);
  801. }
  802. else {
  803. const { host } = this.getInfo();
  804. args.unshift(`--inspect=${(0, utils_1.connectableHostname)(host)}:${this.args.debugPort}`);
  805. }
  806. }
  807. const pnpPath = path.join(backend.functionsDir, ".pnp.js");
  808. if (fs.existsSync(pnpPath)) {
  809. emulatorLogger_1.EmulatorLogger.forEmulator(types_1.Emulators.FUNCTIONS).logLabeled("WARN_ONCE", "functions", "Detected yarn@2 with PnP. " +
  810. "Cloud Functions for Firebase requires a node_modules folder to work correctly and is therefore incompatible with PnP. " +
  811. "See https://yarnpkg.com/getting-started/migration#step-by-step for more information.");
  812. }
  813. const runtimeEnv = this.getRuntimeEnvs(backend, trigger);
  814. const secretEnvs = await this.resolveSecretEnvs(backend, trigger);
  815. const socketPath = (0, functionsEmulatorShared_1.getTemporarySocketPath)();
  816. const childProcess = spawn(backend.nodeBinary, args, {
  817. cwd: backend.functionsDir,
  818. env: Object.assign(Object.assign(Object.assign(Object.assign({ node: backend.nodeBinary }, process.env), runtimeEnv), secretEnvs), { PORT: socketPath }),
  819. stdio: ["pipe", "pipe", "pipe", "ipc"],
  820. });
  821. const runtime = {
  822. process: childProcess,
  823. events: emitter,
  824. cwd: backend.functionsDir,
  825. socketPath,
  826. };
  827. const extensionLogInfo = {
  828. instanceId: backend.extensionInstanceId,
  829. ref: (_a = backend.extensionVersion) === null || _a === void 0 ? void 0 : _a.ref,
  830. };
  831. const pool = this.workerPools[backend.codebase];
  832. const worker = pool.addWorker(trigger === null || trigger === void 0 ? void 0 : trigger.id, runtime, extensionLogInfo);
  833. await worker.waitForSocketReady();
  834. return worker;
  835. }
  836. async disableBackgroundTriggers() {
  837. Object.values(this.triggers).forEach((record) => {
  838. if (record.def.eventTrigger && record.enabled) {
  839. this.logger.logLabeled("BULLET", `functions[${record.def.entryPoint}]`, "function temporarily disabled.");
  840. record.enabled = false;
  841. }
  842. });
  843. await this.workQueue.flush();
  844. }
  845. async reloadTriggers() {
  846. this.triggerGeneration++;
  847. for (const backend of this.args.emulatableBackends) {
  848. await this.loadTriggers(backend);
  849. }
  850. await this.performPostLoadOperations();
  851. return;
  852. }
  853. getEmulatorInfo(emulator) {
  854. if (this.args.remoteEmulators) {
  855. if (this.args.remoteEmulators[emulator]) {
  856. return this.args.remoteEmulators[emulator];
  857. }
  858. }
  859. return registry_1.EmulatorRegistry.getInfo(emulator);
  860. }
  861. tokenFromAuthHeader(authHeader) {
  862. const match = /^Bearer (.*)$/.exec(authHeader);
  863. if (!match) {
  864. return;
  865. }
  866. let idToken = match[1];
  867. logger_1.logger.debug(`ID Token: ${idToken}`);
  868. if (idToken && idToken.includes("=")) {
  869. idToken = idToken.replace(/[=]+?\./g, ".");
  870. logger_1.logger.debug(`ID Token contained invalid padding, new value: ${idToken}`);
  871. }
  872. try {
  873. const decoded = jwt.decode(idToken, { complete: true });
  874. if (!decoded || typeof decoded !== "object") {
  875. logger_1.logger.debug(`Failed to decode ID Token: ${decoded}`);
  876. return;
  877. }
  878. const claims = decoded.payload;
  879. claims.uid = claims.sub;
  880. return claims;
  881. }
  882. catch (e) {
  883. return;
  884. }
  885. }
  886. async handleHttpsTrigger(req, res) {
  887. const method = req.method;
  888. let triggerId = req.params.trigger_name;
  889. if (req.params.region) {
  890. triggerId = `${req.params.region}-${triggerId}`;
  891. }
  892. if (!this.triggers[triggerId]) {
  893. res
  894. .status(404)
  895. .send(`Function ${triggerId} does not exist, valid functions are: ${Object.keys(this.triggers).join(", ")}`);
  896. return;
  897. }
  898. const record = this.getTriggerRecordByKey(triggerId);
  899. if (!record.enabled) {
  900. res.status(204).send("Background triggers are currently disabled.");
  901. return;
  902. }
  903. const trigger = record.def;
  904. logger_1.logger.debug(`Accepted request ${method} ${req.url} --> ${triggerId}`);
  905. const reqBody = req.rawBody;
  906. const isCallable = trigger.labels && trigger.labels["deployment-callable"] === "true";
  907. const authHeader = req.header("Authorization");
  908. if (authHeader && isCallable && trigger.platform !== "gcfv2") {
  909. const token = this.tokenFromAuthHeader(authHeader);
  910. if (token) {
  911. const contextAuth = {
  912. uid: token.uid,
  913. token: token,
  914. };
  915. req.headers[functionsEmulatorShared_1.HttpConstants.ORIGINAL_AUTH_HEADER] = req.headers["authorization"];
  916. delete req.headers["authorization"];
  917. req.headers[functionsEmulatorShared_1.HttpConstants.CALLABLE_AUTH_HEADER] = encodeURIComponent(JSON.stringify(contextAuth));
  918. }
  919. }
  920. void (0, track_1.track)(EVENT_INVOKE, (0, functionsEmulatorShared_1.getFunctionService)(trigger));
  921. void (0, track_1.trackEmulator)(EVENT_INVOKE_GA4, {
  922. function_service: (0, functionsEmulatorShared_1.getFunctionService)(trigger),
  923. });
  924. this.logger.log("DEBUG", `[functions] Runtime ready! Sending request!`);
  925. const url = new url_1.URL(`${req.protocol}://${req.hostname}${req.url}`);
  926. const path = `${url.pathname}${url.search}`.replace(new RegExp(`\/${this.args.projectId}\/[^\/]*\/${req.params.trigger_name}\/?`), "/");
  927. this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`);
  928. const pool = this.workerPools[record.backend.codebase];
  929. if (!pool.readyForWork(trigger.id)) {
  930. await this.startRuntime(record.backend, trigger);
  931. }
  932. const debugBundle = this.args.debugPort
  933. ? {
  934. functionTarget: trigger.entryPoint,
  935. functionSignature: (0, functionsEmulatorShared_1.getSignatureType)(trigger),
  936. }
  937. : undefined;
  938. await pool.submitRequest(trigger.id, {
  939. method,
  940. path,
  941. headers: req.headers,
  942. }, res, reqBody, debugBundle);
  943. }
  944. }
  945. exports.FunctionsEmulator = FunctionsEmulator;