emulatorHost();
+
+ /**
+ * Forces the usage of emulator credentials. The logic automatically uses emulator credentials in case
+ * the emulatorHost is set.
+ *
+ * - If true: force usage of emulator credentials
+ * - If false: force not using emulator credentials
+ *
+ */
+ @WithDefault("true")
+ boolean useEmulatorCredentials();
+
+ }
+
+}
diff --git a/firebase-devservices/README.md b/firebase-devservices/README.md
new file mode 100644
index 00000000..d45ed50c
--- /dev/null
+++ b/firebase-devservices/README.md
@@ -0,0 +1,5 @@
+# Quarkus - Google Cloud Services - Firebase
+
+This extension provides a DevService for the Firebase.
+
+You can find the documentation in the [Firebase Quarkiverse documentation site](https://quarkiverse.github.io/quarkiverse-docs/quarkus-google-cloud-services/main/firebase.html).
diff --git a/firebase-devservices/deployment/pom.xml b/firebase-devservices/deployment/pom.xml
new file mode 100644
index 00000000..f8f5b649
--- /dev/null
+++ b/firebase-devservices/deployment/pom.xml
@@ -0,0 +1,158 @@
+
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-firebase-devservices-parent
+ 2.14.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-google-cloud-firebase-devservices-deployment
+ Quarkus - Google Cloud Services - Firebase Dev Services - Deployment
+
+
+
+ io.quarkus
+ quarkus-core-deployment
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-common-deployment
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-firebase-devservices
+
+
+ org.testcontainers
+ testcontainers
+
+
+
+
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ com.google.firebase
+ firebase-admin
+ test
+
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+
+
+ io.quarkus
+ quarkus-grpc-common
+ test
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-common-grpc
+ test
+
+
+ com.google.cloud
+ google-cloud-firestore
+ test
+
+
+ commons-logging
+ commons-logging
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+
+
+ com.google.cloud
+ google-cloud-pubsub
+ test
+
+
+ commons-logging
+ commons-logging
+
+
+ javax.annotation
+ javax.annotation-api
+
+
+ org.checkerframework
+ checker-qual
+
+
+
+ io.grpc
+ grpc-netty-shaded
+
+
+ org.codehaus.mojo
+ animal-sniffer-annotations
+
+
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+ org.jsonschema2pojo
+ jsonschema2pojo-maven-plugin
+ 1.2.2
+
+ ${basedir}/src/main/resources/META-INF/schema
+ io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.json
+ jackson2
+ true
+
+
+
+
+
+ generate
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseBuildSteps.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseBuildSteps.java
new file mode 100644
index 00000000..96bbc13c
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseBuildSteps.java
@@ -0,0 +1,14 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.builditem.FeatureBuildItem;
+
+public class FirebaseBuildSteps {
+
+ protected static final String FEATURE = "google-cloud-firebase";
+
+ @BuildStep
+ public FeatureBuildItem feature() {
+ return new FeatureBuildItem(FEATURE);
+ }
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java
new file mode 100644
index 00000000..08b58ab6
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceConfig.java
@@ -0,0 +1,293 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import java.util.Optional;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+/**
+ * Root configuration class for Google Cloud Firebase that operates at build time.
+ * This class provides a nested structure for configuration, including
+ * a separate group for the development service configuration.
+ *
+ * Here is an example of how to configure these properties:
+ *
+ *
+ *
+ * quarkus.google.cloud.firebase.devservice.enabled = true
+ * quarkus.google.cloud.pubsub.devservice.image-name = gcr.io/google.com/cloudsdktool/google-cloud-cli # optional
+ * quarkus.google.cloud.pubsub.devservice.emulatorPort = 8085 # optional
+ *
+ */
+@ConfigMapping(prefix = "quarkus.google.cloud.devservices")
+@ConfigRoot
+public interface FirebaseDevServiceConfig {
+
+ /**
+ * Configure the Firebase-based services
+ */
+ Firebase firebase();
+
+ /**
+ * Configuration for the Functions emulator
+ */
+ GenericDevService functions();
+
+ /**
+ * Configuration for the Google Cloud PubSub emulator
+ */
+ GenericDevService pubsub();
+
+ /**
+ * Configuration for the storage emulator
+ */
+ StorageDevService storage();
+
+ interface Firebase {
+
+ /**
+ * Indicates to use the dev service for Firebase. The default value is true. This indicator is used
+ * to detect the Firebase DevService and disable the DevServices for extensions which conflict with the
+ * Firebase DevService.
+ */
+ @WithDefault("true")
+ boolean preferFirebaseDevServices();
+
+ /**
+ * Configuration for the firebase emulator devservice. This is the generic configuration for the firebase
+ * emulator. THe specifics are handled in each of the other dev services.
+ */
+ Emulator emulator();
+
+ /**
+ * Configuration for the firebase auth emulator
+ */
+ GenericDevService auth();
+
+ /**
+ * Configure Firebase Hosting
+ */
+ HostingDevService hosting();
+
+ /**
+ * Configuration for the realtime database emulator
+ */
+ GenericDevService database();
+
+ /**
+ * Configure the firestore
+ */
+ FirestoreDevService firestore();
+
+ interface Emulator {
+
+ /**
+ * The version of the firebase tools to use. Default is to use the latest available version.
+ */
+ @WithDefault(FirebaseEmulatorContainer.DEFAULT_FIREBASE_VERSION)
+ String firebaseVersion();
+
+ /**
+ * Docker specific settings
+ */
+ Docker docker();
+
+ /**
+ * The ClI settings
+ */
+ Cli cli();
+
+ /**
+ * Indicate to use a custom firebase.json file instead of the automatically generated one. The custom
+ * firebase.json file MUST include a setting of
+ *
+ *
+ * "host" : "0.0.0.0"
+ *
+ *
+ * to ensure the ports of the
+ * emulator are exposed correctly at the docker container level.
+ *
+ * See the section on Custom Firebase Json in the docs for more info.
+ */
+ Optional customFirebaseJson();
+
+ /**
+ * Settings for the emulator UI
+ */
+ UI ui();
+
+ interface Docker {
+ /**
+ * Sets the Docker image name for the Google Cloud SDK.
+ * This image is used to emulate the Pub/Sub service in the development environment.
+ * The default value is 'node:23-alpine'.
+ *
+ * See also the documentation on Custom Docker images for more info about this image.
+ */
+ @WithDefault(FirebaseEmulatorContainer.DEFAULT_IMAGE_NAME)
+ String imageName();
+
+ /**
+ * Id of the docker user to run the firebase executable. This is needed in environments where Docker
+ * does not perform a mapping to the user running Docker. In a Docker Desktop setup, Docker
+ * automatically performs this mapping and the data written by the emulator can be read by the user
+ * running the build. This is not the case in a regular (non-Desktop) setup,
+ * so you may need to set the user id and {@link #dockerGroup()}. This option is often needed in CI
+ * environments.
+ */
+ Optional dockerUser();
+
+ /**
+ * Id of the group to which the {@link #dockerUser()} belongs.
+ */
+ Optional dockerGroup();
+
+ /**
+ * Try to read the {@link #dockerUser()} from an environment variable
+ */
+ Optional dockerUserEnv();
+
+ /**
+ * Try to read the {@link #dockerGroup()} from an environment variable
+ */
+ Optional dockerGroupEnv();
+
+ /**
+ * Pipe Stdout of the container to the Quarkus logging
+ */
+ Optional followStdOut();
+
+ /**
+ * Pipe Stedd of the container to the Quarkus logging
+ */
+ Optional followStdErr();
+
+ }
+
+ /**
+ * Configuration options related to the Firebase emulators CLI
+ */
+ interface Cli {
+ /**
+ * The token to use for firebase authentication. Run `firebase login:ci` locally to get a new token. This
+ * option is mandatory if you use firebase hosting.
+ */
+ Optional token();
+
+ /**
+ * Sets the JAVA tool options for emulators based on the Java runtime environment like -Xmx.
+ * See also
+ * here
+ */
+ Optional javaToolOptions();
+
+ /**
+ * Allow to import and export data. Specify a path relative to the current working directory of the executable
+ * (for most unit tests, this is the root of the build directory) to be used for import and export of emulator
+ * data. The data will be written to a subdirectory called "emulator-data" of this directory.
+ * See also here
+ */
+ Optional emulatorData();
+
+ /**
+ * Indicate whether to import, export or both the data specified in {@link #emulatorData()}
+ */
+ Optional importExport();
+
+ /**
+ * Enable firebase emulators debugging.
+ */
+ Optional debug();
+
+ }
+
+ interface UI extends GenericDevService {
+
+ /**
+ * Indicates whether the service should be enabled or not.
+ * The default value is 'false'.
+ */
+ @WithDefault("true")
+ @Override
+ boolean enabled();
+
+ /**
+ * Port on which to expose the logging endpoint port. This is needed in case you want to view the logging
+ * via the Emulator UI.
+ */
+ Optional loggingPort();
+
+ /**
+ * Port on which to expose the hub endpoint port. This is needed if you want to use the hub API of
+ * the Emulator UI.
+ */
+ Optional hubPort();
+ }
+
+ }
+
+ interface HostingDevService extends GenericDevService {
+
+ /**
+ * Path to the hosting files.
+ */
+ Optional hostingPath();
+ }
+
+ /**
+ * Extension for the Firestore dev service. This service can also configure the websocket port.
+ */
+ interface FirestoreDevService extends GenericDevService {
+
+ /**
+ * Port on which to expose the websocket port. This is needed in case the Firestore Emulator UI needs is
+ * used.
+ */
+ Optional websocketPort();
+
+ /**
+ * Path to the firestore.rules file.
+ */
+ Optional rulesFile();
+
+ /**
+ * Path to the firestore.indexes.json file.
+ */
+ Optional indexesFile();
+ }
+ }
+
+ interface StorageDevService extends GenericDevService {
+
+ /**
+ * Path to the storage.rules file.
+ */
+ Optional rulesFile();
+ }
+
+ /**
+ * Internal interface representing a dev service for each of the different emulators part of the Firebase
+ * platform.
+ */
+ interface GenericDevService {
+
+ /**
+ * Indicates whether the DevService should be enabled or not.
+ * The default value is 'false'.
+ */
+ @WithDefault("false")
+ boolean enabled();
+
+ /**
+ * Specifies the emulatorPort on which the service should run in the development environment. The default
+ * is to expose the service on a random port.
+ */
+ Optional emulatorPort();
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java
new file mode 100644
index 00000000..3f39cf72
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProcessor.java
@@ -0,0 +1,196 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import org.jboss.logging.Logger;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
+import io.quarkus.deployment.IsNormal;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.BuildSteps;
+import io.quarkus.deployment.builditem.*;
+import io.quarkus.deployment.console.ConsoleInstalledBuildItem;
+import io.quarkus.deployment.console.StartupLogCompressor;
+import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
+import io.quarkus.deployment.logging.LoggingSetupBuildItem;
+
+/**
+ * Processor responsible for managing Firebase Dev Services.
+ *
+ * The processor starts the Firebase service in case it's not running.
+ */
+@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
+public class FirebaseDevServiceProcessor {
+
+ private static final Logger LOGGER = Logger.getLogger(FirebaseDevServiceProcessor.class.getName());
+
+ // Running dev service instance
+ private static volatile DevServicesResultBuildItem.RunningDevService devService;
+ // Configuration for the Firebase Dev service
+ private static volatile FirebaseDevServiceConfig config;
+
+ private static final Map CONFIG_PROPERTIES = Map.of(
+ FirebaseEmulatorContainer.Emulator.AUTHENTICATION, "quarkus.google.cloud.firebase.auth.emulator-host",
+ FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI, "quarkus.google.cloud.firebase.emulator-host",
+ FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING, "quarkus.google.cloud.firebase.hosting.emulator-host",
+ FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS, "quarkus.google.cloud.functions.emulator-host",
+ FirebaseEmulatorContainer.Emulator.EVENT_ARC, "quarkus.google.cloud.eventarc.emulator-host",
+ FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE, "quarkus.google.cloud.database.emulator-host",
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE, "quarkus.google.cloud.firestore.host-override",
+ FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE, "quarkus.google.cloud.storage.host-override",
+ FirebaseEmulatorContainer.Emulator.PUB_SUB, "quarkus.google.cloud.pubsub.emulator-host");
+
+ @BuildStep
+ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildItem,
+ FirebaseDevServiceProjectConfig projectConfig,
+ FirebaseDevServiceConfig firebaseBuildTimeConfig,
+ List devServicesSharedNetworkBuildItem,
+ Optional consoleInstalledBuildItem,
+ CuratedApplicationShutdownBuildItem closeBuildItem,
+ LaunchModeBuildItem launchMode,
+ LoggingSetupBuildItem loggingSetupBuildItem,
+ GlobalDevServicesConfig globalDevServicesConfig) {
+ // If dev service is running and config has changed, stop the service
+ if (devService != null && !firebaseBuildTimeConfig.equals(config)) {
+ stopContainer();
+ } else if (devService != null) {
+ return devService.toBuildItem();
+ }
+
+ // Set up log compressor for startup logs
+ StartupLogCompressor compressor = new StartupLogCompressor(
+ (launchMode.isTest() ? "(test) " : "") + "Google Cloud Firebase Dev Services Starting:",
+ consoleInstalledBuildItem,
+ loggingSetupBuildItem);
+
+ // Try starting the container if conditions are met
+ try {
+ devService = startContainerIfAvailable(dockerStatusBuildItem, projectConfig, firebaseBuildTimeConfig,
+ globalDevServicesConfig.timeout);
+ } catch (Throwable t) {
+ LOGGER.warn("Unable to start Firebase dev service", t);
+ // Dump captured logs in case of an error
+ compressor.closeAndDumpCaptured();
+ return null;
+ } finally {
+ compressor.close();
+ }
+
+ return devService == null ? null : devService.toBuildItem();
+ }
+
+ /**
+ * Start the container if conditions are met.
+ *
+ * @param dockerStatusBuildItem, Docker status
+ * @param config, Configuration for the Firebase service
+ * @param timeout, Optional timeout for starting the service
+ * @return Running service item, or null if the service couldn't be started
+ */
+ private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(DockerStatusBuildItem dockerStatusBuildItem,
+ FirebaseDevServiceProjectConfig projectConfig,
+ FirebaseDevServiceConfig config,
+ Optional timeout) {
+
+ if (!config.firebase().preferFirebaseDevServices()) {
+ // Firebase service explicitly disabled
+ LOGGER.info("Not starting Dev Services for Firebase as it has been disabled in the config.");
+ return null;
+ }
+
+ if (!isEnabled(config)) {
+ // Firebase service implicitly disabled, no emulators enabled.
+ LOGGER.info("Not starting Dev Services for Firebase as no emulators are enabled.");
+ return null;
+ }
+
+ if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) {
+ LOGGER.info("Not starting DevService because docker is not available");
+ return null;
+ }
+
+ return startContainer(dockerStatusBuildItem, projectConfig, config, timeout);
+ }
+
+ private boolean isEnabled(FirebaseDevServiceConfig config) {
+ return FirebaseEmulatorConfigBuilder.devServices(config)
+ .values()
+ .stream()
+ .map(FirebaseDevServiceConfig.GenericDevService::enabled)
+ .reduce(Boolean.FALSE, Boolean::logicalOr);
+ }
+
+ /**
+ * Starts the Pub/Sub emulator container with provided configuration.
+ *
+ * @param dockerStatusBuildItem, Docker status
+ * @param config, Configuration for the Firebase service
+ * @param timeout, Optional timeout for starting the service
+ * @return Running service item, or null if the service couldn't be started
+ */
+ private DevServicesResultBuildItem.RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem,
+ FirebaseDevServiceProjectConfig projectConfig,
+ FirebaseDevServiceConfig config,
+ Optional timeout) {
+
+ // Create and configure Firebase emulator container
+ var emulatorContainer = new FirebaseEmulatorConfigBuilder(projectConfig, config).build();
+
+ // Set container startup timeout if provided
+ timeout.ifPresent(emulatorContainer::withStartupTimeout);
+ emulatorContainer.start();
+
+ // Set the config for the started container
+ FirebaseDevServiceProcessor.config = config;
+
+ var emulatorContainerConfig = emulatorContainerConfig(emulatorContainer);
+
+ if (LOGGER.isInfoEnabled()) {
+ var runningPorts = emulatorContainer.emulatorPorts();
+ runningPorts.forEach((e, p) -> LOGGER.info("Google Cloud Emulator " + e + " reachable on port " + p));
+
+ emulatorContainerConfig
+ .forEach((e, h) -> LOGGER.info("Google Cloud emulator config property " + e + " set to " + h));
+ }
+
+ // Return running service item with container details
+ return new DevServicesResultBuildItem.RunningDevService(FirebaseBuildSteps.FEATURE,
+ emulatorContainer.getContainerId(),
+ emulatorContainer::close,
+ emulatorContainerConfig);
+ }
+
+ private Map emulatorContainerConfig(FirebaseEmulatorContainer emulatorContainer) {
+ return emulatorContainer.emulatorEndpoints()
+ .entrySet()
+ .stream()
+ .filter(e -> CONFIG_PROPERTIES.containsKey(e.getKey()))
+ .collect(
+ Collectors.toMap(
+ e -> configPropertyForEmulator(e.getKey()),
+ Map.Entry::getValue));
+ }
+
+ private String configPropertyForEmulator(FirebaseEmulatorContainer.Emulator emulator) {
+ return CONFIG_PROPERTIES.get(emulator);
+ }
+
+ /**
+ * Stops the running Firebase emulator container.
+ */
+ private void stopContainer() {
+ if (devService != null && devService.isOwner()) {
+ try {
+ // Try closing the running dev service
+ devService.close();
+ } catch (Throwable e) {
+ LOGGER.error("Failed to stop firebase container", e);
+ } finally {
+ devService = null;
+ }
+ }
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProjectConfig.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProjectConfig.java
new file mode 100644
index 00000000..5ab503dd
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseDevServiceProjectConfig.java
@@ -0,0 +1,20 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+
+/**
+ * Temporary Config root to retrieve the project id for the Firebase Emulator Container. We will remove this interface
+ * in the future in favour of using the common setup.
+ */
+@ConfigMapping(prefix = "quarkus.google.cloud.devservices")
+@ConfigRoot
+public interface FirebaseDevServiceProjectConfig {
+
+ /**
+ * Google Cloud project ID. The project is required to be set if you use the Firebase Auth Dev service.
+ */
+ Optional projectId();
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java
new file mode 100644
index 00000000..e3ed09aa
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilder.java
@@ -0,0 +1,176 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
+
+/**
+ * This class translates the Quarkus Firebase extension configuration to the {@link FirebaseEmulatorContainer}
+ * instance.
+ */
+public class FirebaseEmulatorConfigBuilder {
+
+ private final FirebaseDevServiceProjectConfig projectConfig;
+ private final FirebaseDevServiceConfig config;
+
+ public static Map devServices(
+ FirebaseDevServiceConfig config) {
+ return Map.of(
+ FirebaseEmulatorContainer.Emulator.AUTHENTICATION, config.firebase().auth(),
+ FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI, config.firebase().emulator().ui(),
+ FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS, config.functions(),
+ FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE, config.firebase().database(),
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE, config.firebase().firestore(),
+ FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE, config.storage(),
+ FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING, config.firebase().hosting(),
+ FirebaseEmulatorContainer.Emulator.PUB_SUB, config.pubsub());
+ }
+
+ public FirebaseEmulatorConfigBuilder(FirebaseDevServiceProjectConfig projectConfig, FirebaseDevServiceConfig config) {
+ this.projectConfig = projectConfig;
+ this.config = config;
+ }
+
+ public FirebaseEmulatorContainer build() {
+ return configureBuilder().build();
+ }
+
+ FirebaseEmulatorContainer.EmulatorConfig buildConfig() {
+ return configureBuilder().buildConfig();
+ }
+
+ private FirebaseEmulatorContainer.Builder configureBuilder() {
+ var builder = FirebaseEmulatorContainer.builder();
+
+ builder.withFirebaseVersion(config.firebase().emulator().firebaseVersion());
+
+ handleDockerConfig(config.firebase().emulator().docker(), builder);
+ handleCliConfig(config.firebase().emulator().cli(), builder);
+ handleEmulators(builder);
+
+ return builder;
+ }
+
+ private void handleDockerConfig(FirebaseDevServiceConfig.Firebase.Emulator.Docker docker,
+ FirebaseEmulatorContainer.Builder builder) {
+ var dockerConfig = builder.withDockerConfig();
+
+ dockerConfig.withImage(docker.imageName());
+ docker.dockerUser().ifPresent(dockerConfig::withUserId);
+ docker.dockerGroup().ifPresent(dockerConfig::withGroupId);
+ docker.dockerUserEnv().ifPresent(dockerConfig::withUserIdFromEnv);
+ docker.dockerGroupEnv().ifPresent(dockerConfig::withGroupIdFromEnv);
+ docker.followStdOut().ifPresent(dockerConfig::followStdOut);
+ docker.followStdErr().ifPresent(dockerConfig::followStdErr);
+
+ dockerConfig.done();
+ }
+
+ private void handleCliConfig(FirebaseDevServiceConfig.Firebase.Emulator.Cli cli,
+ FirebaseEmulatorContainer.Builder builder) {
+ var cliConfig = builder.withCliArguments();
+
+ projectConfig.projectId().ifPresent(cliConfig::withProjectId);
+
+ cli.token().ifPresent(cliConfig::withToken);
+ cli.javaToolOptions().ifPresent(cliConfig::withJavaToolOptions);
+ cli.emulatorData().map(FirebaseEmulatorConfigBuilder::asPath).ifPresent(cliConfig::withEmulatorData);
+ cli.importExport().ifPresent(cliConfig::withImportExport);
+ cli.debug().ifPresent(cliConfig::withDebug);
+
+ cliConfig.done();
+ }
+
+ private void handleEmulators(FirebaseEmulatorContainer.Builder builder) {
+ config.firebase().emulator().customFirebaseJson().ifPresentOrElse(
+ (firebaseJson) -> configureCustomFirebaseJson(builder, firebaseJson),
+ () -> configureEmulators(builder));
+ }
+
+ private void configureCustomFirebaseJson(FirebaseEmulatorContainer.Builder builder, String firebaseJson) {
+ try {
+ builder.readFromFirebaseJson(asPath(firebaseJson));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void configureEmulators(FirebaseEmulatorContainer.Builder builder) {
+ var devServices = devServices(config);
+
+ var noEmulatorsConfigured = devServices
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue().enabled())
+ // Emulator Suite UI is enabled by default, so ignore it.
+ .filter(e -> !e.getKey().equals(FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI))
+ .findAny()
+ .isEmpty();
+
+ // No emulators configured via configuration, we will fallback to the automatic detection of a firebase.json file.
+ if (noEmulatorsConfigured) {
+ return;
+ }
+
+ var firebaseConfigBuilder = builder.withFirebaseConfig();
+ devServices
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue().enabled())
+ .forEach(e -> e.getValue().emulatorPort().ifPresentOrElse(
+ p -> firebaseConfigBuilder.withEmulatorOnFixedPort(e.getKey(), p),
+ () -> firebaseConfigBuilder.withEmulator(e.getKey())));
+
+ config.firebase()
+ .hosting()
+ .hostingPath()
+ .map(FirebaseEmulatorConfigBuilder::asPath)
+ .ifPresent(firebaseConfigBuilder::withHostingPath);
+
+ config.firebase()
+ .firestore()
+ .indexesFile()
+ .map(FirebaseEmulatorConfigBuilder::asPath)
+ .ifPresent(firebaseConfigBuilder::withFirestoreIndexes);
+
+ config.firebase()
+ .firestore()
+ .rulesFile()
+ .map(FirebaseEmulatorConfigBuilder::asPath)
+ .ifPresent(firebaseConfigBuilder::withFirestoreRules);
+
+ config.firebase()
+ .firestore()
+ .websocketPort()
+ .ifPresent(p -> firebaseConfigBuilder
+ .withEmulatorOnFixedPort(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS, p));
+
+ config.firebase()
+ .emulator()
+ .ui()
+ .hubPort()
+ .ifPresent(
+ p -> firebaseConfigBuilder.withEmulatorOnFixedPort(FirebaseEmulatorContainer.Emulator.EMULATOR_HUB, p));
+
+ config.firebase()
+ .emulator()
+ .ui()
+ .loggingPort()
+ .ifPresent(p -> firebaseConfigBuilder.withEmulatorOnFixedPort(FirebaseEmulatorContainer.Emulator.LOGGING, p));
+
+ config.storage()
+ .rulesFile()
+ .map(FirebaseEmulatorConfigBuilder::asPath)
+ .ifPresent(firebaseConfigBuilder::withStorageRules);
+
+ firebaseConfigBuilder.done();
+ }
+
+ private static Path asPath(String path) {
+ return new File(path).toPath();
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java
new file mode 100644
index 00000000..a7050bd9
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/CustomFirebaseConfigReader.java
@@ -0,0 +1,217 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.json.Emulators;
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.json.FirebaseConfig;
+
+/**
+ * Reader for the firebase.json file to convert it to the
+ * {@link io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer.FirebaseConfig}
+ */
+class CustomFirebaseConfigReader {
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ /**
+ * Read the firebase config from a firebase.json file
+ *
+ * @param customFirebaseJson The path to the file
+ * @return The configuration
+ * @throws IOException In case the file could not be read
+ */
+ public FirebaseEmulatorContainer.FirebaseConfig readFromFirebase(Path customFirebaseJson) throws IOException {
+ var root = readCustomFirebaseJson(customFirebaseJson);
+
+ return new FirebaseEmulatorContainer.FirebaseConfig(
+ readHosting(root.getHosting(), customFirebaseJson),
+ readStorage(root.getStorage(), customFirebaseJson),
+ readFirestore(root.getFirestore(), customFirebaseJson),
+ readFunctions(root.getFunctions(), customFirebaseJson),
+ readEmulators(root.getEmulators()));
+ }
+
+ private record EmulatorMergeStrategy(
+ FirebaseEmulatorContainer.Emulator emulator,
+ Supplier configObjectSupplier,
+ Function> portSupplier) {
+ }
+
+ private Map readEmulators(Emulators em) {
+ var mergeStrategies = new EmulatorMergeStrategy>[] {
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.AUTHENTICATION,
+ em::getAuth,
+ a -> a::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI,
+ em::getUi,
+ u -> u::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.EMULATOR_HUB,
+ em::getHub,
+ h -> h::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.LOGGING,
+ em::getLogging,
+ l -> l::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS,
+ em::getFunctions,
+ f -> f::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.EVENT_ARC,
+ em::getEventarc,
+ e -> e::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE,
+ em::getDatabase,
+ d -> d::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE,
+ em::getFirestore,
+ d -> d::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE,
+ em::getStorage,
+ s -> s::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING,
+ em::getHosting,
+ h -> h::getPort),
+ new EmulatorMergeStrategy<>(
+ FirebaseEmulatorContainer.Emulator.PUB_SUB,
+ em::getPubsub,
+ h -> h::getPort)
+ };
+
+ var map = Arrays.stream(mergeStrategies)
+ .map(this::mergeEmulator)
+ .filter(e -> !Objects.isNull(e))
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+ if (em.getFirestore() != null && em.getFirestore().getWebsocketPort() != null) {
+ map = new HashMap<>(map);
+
+ map.put(
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS,
+ new FirebaseEmulatorContainer.ExposedPort(em.getFirestore().getWebsocketPort()));
+
+ map = Map.copyOf(map);
+ }
+
+ return map;
+ }
+
+ private Map.Entry mergeEmulator(
+ EmulatorMergeStrategy emulatorMergeStrategy) {
+
+ var configObject = emulatorMergeStrategy.configObjectSupplier.get();
+ if (configObject != null) {
+ var port = emulatorMergeStrategy.portSupplier.apply(configObject).get();
+ return Map.entry(emulatorMergeStrategy.emulator, new FirebaseEmulatorContainer.ExposedPort(port));
+ } else {
+ return null;
+ }
+ }
+
+ private FirebaseEmulatorContainer.FirestoreConfig readFirestore(Object firestore, Path customFirebaseJson) {
+ if (firestore instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map firestoreMap = (Map) firestore;
+
+ var rulesFile = Optional
+ .ofNullable(firestoreMap.get("rules"))
+ .map(f -> this.resolvePath(f, customFirebaseJson));
+ var indexesFile = Optional
+ .ofNullable(firestoreMap.get("indexes"))
+ .map(f -> this.resolvePath(f, customFirebaseJson));
+
+ return new FirebaseEmulatorContainer.FirestoreConfig(
+ rulesFile,
+ indexesFile);
+ } else {
+ return FirebaseEmulatorContainer.FirestoreConfig.DEFAULT;
+ }
+ }
+
+ private FirebaseEmulatorContainer.HostingConfig readHosting(Object hosting, Path customFirebaseJson) {
+ if (hosting instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map hostingMap = (Map) hosting;
+
+ var publicDir = Optional
+ .ofNullable(hostingMap.get("public"))
+ .map(f -> this.resolvePath(f, customFirebaseJson));
+
+ return new FirebaseEmulatorContainer.HostingConfig(
+ publicDir);
+ } else {
+ return FirebaseEmulatorContainer.HostingConfig.DEFAULT;
+ }
+ }
+
+ private FirebaseEmulatorContainer.StorageConfig readStorage(Object storage, Path customFirebaseJson) {
+ if (storage instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map storageMap = (Map) storage;
+
+ var rulesFile = Optional
+ .ofNullable(storageMap.get("rules"))
+ .map(f -> this.resolvePath(f, customFirebaseJson));
+
+ return new FirebaseEmulatorContainer.StorageConfig(
+ rulesFile);
+ } else {
+ return FirebaseEmulatorContainer.StorageConfig.DEFAULT;
+ }
+ }
+
+ private FirebaseEmulatorContainer.FunctionsConfig readFunctions(Object functions, Path customFirebaseJson) {
+ if (functions instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map functionsMap = (Map) functions;
+
+ var functionsPath = Optional
+ .ofNullable(functionsMap.get("source"))
+ .map(String.class::cast)
+ .map(f -> this.resolvePath(f, customFirebaseJson));
+
+ var ignores = Optional
+ .ofNullable(functionsMap.get("ignores"))
+ .map(String[].class::cast)
+ .orElse(new String[0]);
+
+ return new FirebaseEmulatorContainer.FunctionsConfig(
+ functionsPath,
+ ignores);
+ } else {
+ return FirebaseEmulatorContainer.FunctionsConfig.DEFAULT;
+ }
+ }
+
+ private Path resolvePath(String filename, Path customFirebaseJson) {
+ File firebaseJson = customFirebaseJson.toFile();
+ File firebaseDir = firebaseJson.getParentFile();
+ return new File(firebaseDir, filename).toPath();
+ }
+
+ private FirebaseConfig readCustomFirebaseJson(Path customFirebaseJson) throws IOException {
+ var customFirebaseFile = customFirebaseJson.toFile();
+ var customFirebaseStream = new BufferedInputStream(new FileInputStream(customFirebaseFile));
+
+ return objectMapper.readerFor(FirebaseConfig.class)
+ .readValue(customFirebaseStream);
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java
new file mode 100644
index 00000000..31c96003
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainer.java
@@ -0,0 +1,1389 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.event.Level;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.output.OutputFrame;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.images.builder.ImageFromDockerfile;
+import org.testcontainers.images.builder.dockerfile.DockerfileBuilder;
+
+/**
+ * Testcontainers container to run Firebase emulators from Docker.
+ */
+public class FirebaseEmulatorContainer extends GenericContainer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FirebaseEmulatorContainer.class);
+
+ private static final String FIREBASE_ROOT = "/srv/firebase";
+ private static final String FIREBASE_HOSTING_PATH = FIREBASE_ROOT + "/" + FirebaseJsonBuilder.FIREBASE_HOSTING_SUBPATH;
+ private static final String EMULATOR_DATA_PATH = FIREBASE_ROOT + "/data";
+ private static final String EMULATOR_EXPORT_PATH = EMULATOR_DATA_PATH + "/emulator-data";
+
+ /**
+ * Set of possible emulators (or components/services).
+ */
+ public enum Emulator {
+ /**
+ * Firebase Auth emulator
+ */
+ AUTHENTICATION(
+ 9099,
+ "auth",
+ "auth"),
+ /**
+ * Emulator UI, not a real emulator, but allows exposing the UI on a predefined port
+ */
+ EMULATOR_SUITE_UI(
+ 4000,
+ "ui",
+ "ui"),
+ /**
+ * Emulator Hub API port
+ */
+ EMULATOR_HUB(
+ 4400,
+ "hub",
+ null),
+ /**
+ * Emulator UI Logging endpoint
+ */
+ LOGGING(
+ 4500,
+ "logging",
+ null),
+ /**
+ * CLoud functions emulator
+ */
+ CLOUD_FUNCTIONS(
+ 5001,
+ "functions",
+ "functions"),
+ /**
+ * Event Arc emulator
+ */
+ EVENT_ARC(
+ 9299,
+ "eventarc",
+ "eventarc"),
+ /**
+ * Realtime database emulator
+ */
+ REALTIME_DATABASE(
+ 9000,
+ "database",
+ "database"),
+ /**
+ * Firestore emulator
+ */
+ CLOUD_FIRESTORE(
+ 8080,
+ "firestore",
+ "firestore"),
+ /**
+ * Firestore websocket port, This emulator always need to be specified in conjunction with CLOUD_FIRESTORE.
+ */
+ CLOUD_FIRESTORE_WS(
+ 9150,
+ null,
+ null),
+ /**
+ * Cloud storage emulator
+ */
+ CLOUD_STORAGE(
+ 9199,
+ "storage",
+ "storage"),
+ /**
+ * Firebase hosting emulator
+ */
+ FIREBASE_HOSTING(
+ 5000,
+ "hosting",
+ "hosting"),
+ /**
+ * Pub/sub emulator
+ */
+ PUB_SUB(
+ 8085,
+ "pubsub",
+ "pubsub");
+
+ /**
+ * The default port on which the emulator is running.
+ */
+ public final int internalPort;
+ final String configProperty;
+ final String emulatorName;
+
+ Emulator(int internalPort, String configProperty, String onlyArgument) {
+ this.internalPort = internalPort;
+ this.configProperty = configProperty;
+ this.emulatorName = onlyArgument;
+ }
+
+ }
+
+ /**
+ * Record to hold an exposed port of an emulator.
+ *
+ * @param fixedPort The exposed port or null in case it is a random port
+ */
+ public record ExposedPort(Integer fixedPort) {
+
+ public static final ExposedPort RANDOM_PORT = new ExposedPort(null);
+
+ boolean isFixed() {
+ return fixedPort != null;
+ }
+ }
+
+ /**
+ * The docker image configuration
+ *
+ * @param imageName The name of the docker image
+ * @param userId The user id to run the docker image
+ * @param groupId The group id to run the docker image
+ * @param followStdOut Pipe stdout of the container to stdout of the host
+ * @param followStdErr Pipe stderr of the container to stderr of the host
+ * @param afterStart Callback to handle additional logic after the container has started.
+ */
+ public record DockerConfig(
+ String imageName,
+ Optional userId,
+ Optional groupId,
+ boolean followStdOut,
+ boolean followStdErr,
+ Consumer afterStart) {
+
+ /**
+ * Default settings
+ */
+ public static final DockerConfig DEFAULT = new DockerConfig(
+ DEFAULT_IMAGE_NAME,
+ Optional.empty(),
+ Optional.empty(),
+ true,
+ true,
+ null);
+ }
+
+ /**
+ * Record to hold the argument for the CLI executable.
+ *
+ * @param projectId The project ID, needed when running with the auth emulator
+ * @param token The Google Cloud CLI token to use for authentication. Needed for firebase hosting
+ * @param javaToolOptions The options to pass to the java based emulators
+ * @param emulatorData The path to the directory where to store the emulator data
+ * @param importExport Specify whether to import, export or do both with the emulator data
+ * @param debug Whether to run with the --debug flag
+ */
+ public record CliArgumentsConfig(
+ Optional projectId,
+ Optional token,
+ Optional javaToolOptions,
+ Optional emulatorData,
+ ImportExport importExport,
+ boolean debug) {
+ public static final CliArgumentsConfig DEFAULT = new CliArgumentsConfig(
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.empty(),
+ ImportExport.IMPORT_EXPORT,
+ false);
+ }
+
+ /**
+ * Behaviour of the import/export data.
+ */
+ public enum ImportExport {
+ /**
+ * Only import the data
+ */
+ IMPORT_ONLY(true, false),
+
+ /**
+ * Only export the data
+ */
+ EXPORT_ONLY(false, true),
+
+ /**
+ * Both import and export the data.
+ */
+ IMPORT_EXPORT(true, true);
+
+ private final boolean doImport;
+ private final boolean doExport;
+
+ ImportExport(boolean doImport, boolean doExport) {
+ this.doImport = doImport;
+ this.doExport = doExport;
+ }
+
+ boolean isDoImport() {
+ return doImport;
+ }
+
+ boolean isDoExport() {
+ return doExport;
+ }
+ }
+
+ /**
+ * Firebase hosting configuration
+ *
+ * @param hostingContentDir The path to the directory containing the hosting content
+ */
+ public record HostingConfig(
+ Optional hostingContentDir) {
+
+ public static final HostingConfig DEFAULT = new HostingConfig(
+ Optional.empty());
+ }
+
+ /**
+ * Cloud storage configuration
+ *
+ * @param rulesFile Cloud storage rules file
+ */
+ public record StorageConfig(
+ Optional rulesFile) {
+
+ public static final StorageConfig DEFAULT = new StorageConfig(
+ Optional.empty());
+ }
+
+ /**
+ * Firestore configuration
+ *
+ * @param rulesFile The rules file
+ * @param indexesFile The indexes file
+ */
+ public record FirestoreConfig(
+ Optional rulesFile,
+ Optional indexesFile) {
+
+ public static final FirestoreConfig DEFAULT = new FirestoreConfig(
+ Optional.empty(),
+ Optional.empty());
+ }
+
+ /**
+ * Functions configuration
+ *
+ * @param functionsPath The location for the functions sources
+ * @param ignores The files to ignore when creating the function
+ */
+ public record FunctionsConfig(
+ Optional functionsPath,
+ String[] ignores) {
+
+ public static FunctionsConfig DEFAULT = new FunctionsConfig(
+ Optional.empty(),
+ new String[0]);
+ }
+
+ /**
+ * The firebase configuration, this record mimics the various items which can be configured using the
+ * firebase.json file.
+ *
+ * @param hostingConfig The firebase hosting configuration
+ * @param storageConfig The storage configuration
+ * @param firestoreConfig The firestore configuration
+ * @param functionsConfig The functions configuration
+ * @param services The exposed services configuration
+ */
+ public record FirebaseConfig(
+ HostingConfig hostingConfig,
+ StorageConfig storageConfig,
+ FirestoreConfig firestoreConfig,
+ FunctionsConfig functionsConfig,
+ Map services) {
+ }
+
+ /**
+ * Describes the Firebase emulator configuration.
+ *
+ * @param dockerConfig The docker configuration
+ * @param firebaseVersion The firebase version to use
+ * @param cliArguments The arguments to the CLI
+ * @param customFirebaseJson The path to a custom firebase
+ * @param firebaseConfig The firebase configuration
+ */
+ public record EmulatorConfig(
+ DockerConfig dockerConfig,
+ String firebaseVersion,
+ CliArgumentsConfig cliArguments,
+ Optional customFirebaseJson,
+ FirebaseConfig firebaseConfig) {
+ }
+
+ // Use node:20 for now because of https://github.com/firebase/firebase-tools/issues/7173
+ /**
+ * The default image to use for building the docker image.
+ */
+ public static final String DEFAULT_IMAGE_NAME = "node:20-alpine";
+ /**
+ * The default version of the firebase tools to install.
+ */
+ public static final String DEFAULT_FIREBASE_VERSION = "latest";
+
+ /**
+ * Builder for the {@link FirebaseEmulatorContainer} configuration.
+ */
+ public static class Builder {
+
+ private DockerConfig dockerConfig = DockerConfig.DEFAULT;
+ private String firebaseVersion = DEFAULT_FIREBASE_VERSION;
+ private CliArgumentsConfig cliArguments = CliArgumentsConfig.DEFAULT;
+
+ private Path customFirebaseJson;
+ private FirebaseConfig firebaseConfig;
+
+ private Builder() {
+ }
+
+ /**
+ * Configure the docker options
+ *
+ * @return THe docker config builder
+ */
+ public DockerConfigBuilder withDockerConfig() {
+ return new DockerConfigBuilder();
+ }
+
+ /**
+ * Configure the firebase version
+ *
+ * @param firebaseVersion The firebase version
+ * @return The builder
+ */
+ public Builder withFirebaseVersion(String firebaseVersion) {
+ this.firebaseVersion = firebaseVersion;
+ return this;
+ }
+
+ /**
+ * Configure the CLI argument options
+ *
+ * @return The CLI Builder
+ */
+ public CliBuilder withCliArguments() {
+ return new CliBuilder();
+ }
+
+ /**
+ * Read the configuration from the custom firebase.json file.
+ *
+ * @param customFirebaseJson The path to the custom firebase json
+ * @return The builder
+ * @throws IOException In case the file could not be read.
+ */
+ public Builder readFromFirebaseJson(Path customFirebaseJson) throws IOException {
+ var reader = new CustomFirebaseConfigReader();
+ this.firebaseConfig = reader.readFromFirebase(customFirebaseJson);
+ this.customFirebaseJson = customFirebaseJson;
+ return this;
+ }
+
+ /**
+ * Configure the firebase emulators
+ *
+ * @return The firebase config builder
+ */
+ public FirebaseConfigBuilder withFirebaseConfig() {
+ return new FirebaseConfigBuilder();
+ }
+
+ /**
+ * Build the configuration
+ *
+ * @return The emulator configuration.
+ */
+ public EmulatorConfig buildConfig() {
+ if (firebaseConfig == null) {
+ // Try to autoload the firebase.json configuration
+ var defaultFirebaseJson = new File("firebase.json").getAbsoluteFile().toPath();
+
+ LOGGER.info("Trying to automatically read firebase config from {}", defaultFirebaseJson);
+
+ try {
+ readFromFirebaseJson(defaultFirebaseJson);
+ } catch (IOException e) {
+ throw new IllegalStateException(
+ "Firebase was not configured and could not auto-read from " + defaultFirebaseJson);
+ }
+ }
+
+ return new EmulatorConfig(
+ dockerConfig,
+ firebaseVersion,
+ cliArguments,
+ Optional.ofNullable(customFirebaseJson),
+ firebaseConfig);
+ }
+
+ /**
+ * Build the final configuration
+ *
+ * @return the final configuration.
+ */
+ public FirebaseEmulatorContainer build() {
+ return new FirebaseEmulatorContainer(buildConfig());
+ }
+
+ /**
+ * Builder for the docker configuration.
+ */
+ public class DockerConfigBuilder {
+
+ private DockerConfigBuilder() {
+ }
+
+ /**
+ * Configure the base image to use
+ *
+ * @param imageName The image name
+ * @return The builder
+ */
+ public DockerConfigBuilder withImage(String imageName) {
+ Builder.this.dockerConfig = new DockerConfig(
+ imageName,
+ Builder.this.dockerConfig.userId(),
+ Builder.this.dockerConfig.groupId(),
+ Builder.this.dockerConfig.followStdOut(),
+ Builder.this.dockerConfig.followStdErr(),
+ Builder.this.dockerConfig.afterStart());
+ return this;
+ }
+
+ /**
+ * Configure the user id to use within docker
+ *
+ * @param userId The user id
+ * @return The builder
+ */
+ public DockerConfigBuilder withUserId(int userId) {
+ return withUserId(Optional.of(userId));
+ }
+
+ /**
+ * Try to configure the user id to use within docker from an environment variable.
+ *
+ * @param env The environment variable
+ * @return The builder
+ */
+ public DockerConfigBuilder withUserIdFromEnv(String env) {
+ return withUserId(readIdFromEnv(env));
+ }
+
+ private DockerConfigBuilder withUserId(Optional userId) {
+ Builder.this.dockerConfig = new DockerConfig(
+ Builder.this.dockerConfig.imageName(),
+ userId,
+ Builder.this.dockerConfig.groupId(),
+ Builder.this.dockerConfig.followStdOut(),
+ Builder.this.dockerConfig.followStdErr(),
+ Builder.this.dockerConfig.afterStart());
+ return this;
+ }
+
+ /**
+ * Configure the group id to use within docker
+ *
+ * @param groupId The group id
+ * @return The builder
+ */
+ public DockerConfigBuilder withGroupId(int groupId) {
+ return withGroupId(Optional.of(groupId));
+ }
+
+ /**
+ * Try to configure the group id to use within docker from an environment variable.
+ *
+ * @param env The environment variable
+ * @return The builder
+ */
+ public DockerConfigBuilder withGroupIdFromEnv(String env) {
+ return withGroupId(readIdFromEnv(env));
+ }
+
+ private DockerConfigBuilder withGroupId(Optional groupId) {
+ Builder.this.dockerConfig = new DockerConfig(
+ Builder.this.dockerConfig.imageName(),
+ Builder.this.dockerConfig.userId(),
+ groupId,
+ Builder.this.dockerConfig.followStdOut(),
+ Builder.this.dockerConfig.followStdErr(),
+ Builder.this.dockerConfig.afterStart());
+ return this;
+ }
+
+ /**
+ * Pipe the container stdout to the host stdout. This can ease debugging of container issues.
+ *
+ * @param followStdOut Whether to pipe the container stdout to the host stdout
+ * @return The builder
+ */
+ public DockerConfigBuilder followStdOut(boolean followStdOut) {
+ Builder.this.dockerConfig = new DockerConfig(
+ Builder.this.dockerConfig.imageName(),
+ Builder.this.dockerConfig.userId(),
+ Builder.this.dockerConfig.groupId(),
+ followStdOut,
+ Builder.this.dockerConfig.followStdErr(),
+ Builder.this.dockerConfig.afterStart());
+ return this;
+ }
+
+ /**
+ * Pipe the container stdout to the host stderr. This can ease debugging of container issues.
+ *
+ * @param followStdErr Whether to pipe the container stderr to the host stdout
+ * @return The builder
+ */
+ public DockerConfigBuilder followStdErr(boolean followStdErr) {
+ Builder.this.dockerConfig = new DockerConfig(
+ Builder.this.dockerConfig.imageName(),
+ Builder.this.dockerConfig.userId(),
+ Builder.this.dockerConfig.groupId(),
+ Builder.this.dockerConfig.followStdOut(),
+ followStdErr,
+ Builder.this.dockerConfig.afterStart());
+ return this;
+ }
+
+ /**
+ * Set a callback to run after the container has started.
+ *
+ * @param afterStart Callback to be executed after the container has started
+ * @return The builder
+ */
+ public DockerConfigBuilder afterStart(Consumer afterStart) {
+ Builder.this.dockerConfig = new DockerConfig(
+ Builder.this.dockerConfig.imageName(),
+ Builder.this.dockerConfig.userId(),
+ Builder.this.dockerConfig.groupId(),
+ Builder.this.dockerConfig.followStdOut(),
+ Builder.this.dockerConfig.followStdErr(),
+ afterStart);
+ return this;
+ }
+
+ /**
+ * Finish the docker configuration
+ *
+ * @return The primary builder
+ */
+ public Builder done() {
+ return Builder.this;
+ }
+
+ private Optional readIdFromEnv(String env) {
+ try {
+ return Optional
+ .ofNullable(System.getenv(env))
+ .map(Integer::valueOf);
+ } catch (NumberFormatException e) {
+ return Optional.empty();
+ }
+ }
+ }
+
+ /**
+ * Builder for the CLI Arguments configuration
+ */
+ public class CliBuilder {
+ private String projectId;
+ private String token;
+ private String javaToolOptions;
+ private Path emulatorData;
+ private ImportExport importExport;
+ private boolean debug;
+
+ /**
+ * The CLI Builder constructor
+ */
+ private CliBuilder() {
+ this.projectId = Builder.this.cliArguments.projectId.orElse(null);
+ this.token = Builder.this.cliArguments.token.orElse(null);
+ this.javaToolOptions = Builder.this.cliArguments.javaToolOptions.orElse(null);
+ this.emulatorData = Builder.this.cliArguments.emulatorData.orElse(null);
+ this.importExport = Builder.this.cliArguments.importExport;
+ this.debug = Builder.this.cliArguments.debug;
+ }
+
+ /**
+ * Configure the project id
+ *
+ * @param projectId The project id
+ * @return The builder
+ */
+ public CliBuilder withProjectId(String projectId) {
+ this.projectId = projectId;
+ return this;
+ }
+
+ /**
+ * Configure the Google auth token to use
+ *
+ * @param token The token
+ * @return The builder
+ */
+ public CliBuilder withToken(String token) {
+ this.token = token;
+ return this;
+ }
+
+ /**
+ * Configure the java tool options
+ *
+ * @param javaToolOptions The java tool options
+ * @return The builder
+ */
+ public CliBuilder withJavaToolOptions(String javaToolOptions) {
+ this.javaToolOptions = javaToolOptions;
+ return this;
+ }
+
+ /**
+ * Configure the location to import/export the emulator data
+ *
+ * @param emulatorData The emulator data
+ * @return The builder
+ */
+ public CliBuilder withEmulatorData(Path emulatorData) {
+ this.emulatorData = emulatorData;
+ return this;
+ }
+
+ /**
+ * Set the import/export behaviour for the specified emulator data. This setting is inactive unless
+ * {@link #withEmulatorData(Path)} is set.
+ *
+ * @param importExport The import/export setting
+ * @return The builder
+ */
+ public CliBuilder withImportExport(ImportExport importExport) {
+ this.importExport = importExport;
+ return this;
+ }
+
+ /**
+ * Run the firebase tools with a debug flag
+ *
+ * @param debug Whether to run with debug or not
+ * @return The builder
+ */
+ public CliBuilder withDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ /**
+ * Finish the builder
+ *
+ * @return The parent builder
+ */
+ public Builder done() {
+ Builder.this.cliArguments = new CliArgumentsConfig(
+ Optional.ofNullable(this.projectId),
+ Optional.ofNullable(this.token),
+ Optional.ofNullable(this.javaToolOptions),
+ Optional.ofNullable(this.emulatorData),
+ this.importExport,
+ this.debug);
+ return Builder.this;
+ }
+ }
+
+ /**
+ * Builder for the Firebase configuration
+ */
+ public class FirebaseConfigBuilder {
+
+ private HostingConfig hostingConfig = HostingConfig.DEFAULT;
+ private StorageConfig storageConfig = StorageConfig.DEFAULT;
+ private FirestoreConfig firestoreConfig = FirestoreConfig.DEFAULT;
+ private FunctionsConfig functionsConfig = FunctionsConfig.DEFAULT;
+ private final Map services = new HashMap<>();
+
+ /**
+ * Create a new builder
+ */
+ public FirebaseConfigBuilder() {
+ }
+
+ /**
+ * Configure the directory where to find the hosting files
+ *
+ * @param hostingContentDir The hosting directory
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withHostingPath(Path hostingContentDir) {
+ this.hostingConfig = new HostingConfig(
+ Optional.of(hostingContentDir));
+ return this;
+ }
+
+ /**
+ * Configure the Google Cloud storage rules file
+ *
+ * @param rulesFile The rules file.
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withStorageRules(Path rulesFile) {
+ this.storageConfig = new StorageConfig(
+ Optional.of(rulesFile));
+ return this;
+ }
+
+ /**
+ * Configure the Firestore rules file
+ *
+ * @param rulesFile The rules file
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withFirestoreRules(Path rulesFile) {
+ this.firestoreConfig = new FirestoreConfig(
+ Optional.of(rulesFile),
+ this.firestoreConfig.indexesFile);
+ return this;
+ }
+
+ /**
+ * Configure the firestore indexes file
+ *
+ * @param indexes The indexes file
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withFirestoreIndexes(Path indexes) {
+ this.firestoreConfig = new FirestoreConfig(
+ this.firestoreConfig.rulesFile(),
+ Optional.of(indexes));
+ return this;
+ }
+
+ /**
+ * Configure the input directory for the functions
+ *
+ * @param functions The path to the functions
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withFunctionsFromPath(Path functions) {
+ this.functionsConfig = new FunctionsConfig(
+ Optional.of(functions),
+ this.functionsConfig.ignores());
+ return this;
+ }
+
+ /**
+ * Configure the ignores for the functions directory
+ *
+ * @param ignores The ignores
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withFunctionIgnores(String[] ignores) {
+ this.functionsConfig = new FunctionsConfig(
+ this.functionsConfig.functionsPath,
+ ignores);
+ return this;
+ }
+
+ /**
+ * Include an emulator on a random port
+ *
+ * @param emulator The emulator
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withEmulator(Emulator emulator) {
+ this.services.put(emulator, ExposedPort.RANDOM_PORT);
+ return this;
+ }
+
+ /**
+ * Include emulators on a random port
+ *
+ * @param emulators The emulators
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withEmulators(Emulator... emulators) {
+ for (Emulator emulator : emulators) {
+ withEmulator(emulator);
+ }
+ return this;
+ }
+
+ /**
+ * Include an emulator on a fixed port
+ *
+ * @param emulator The emulator
+ * @param port The port to expose on
+ * @return The builder
+ */
+ public FirebaseConfigBuilder withEmulatorOnFixedPort(Emulator emulator, int port) {
+ this.services.put(emulator, new ExposedPort(port));
+ return this;
+ }
+
+ /**
+ * Include emulators on fixed ports
+ *
+ * @param emulatorsAndPorts Alternating the {@link Emulator} and the {@link Integer} port.
+ * @return The builder
+ * @throws IllegalArgumentException In case the arguments don't alternate between Emulator and Port.
+ */
+ public FirebaseConfigBuilder withEmulatorsOnPorts(Object... emulatorsAndPorts) {
+ if (emulatorsAndPorts.length % 2 != 0) {
+ throw new IllegalArgumentException("Emulators and ports must both be specified alternating");
+ }
+
+ try {
+ for (int i = 0; i < emulatorsAndPorts.length; i += 2) {
+ var emulator = (Emulator) emulatorsAndPorts[i];
+ var port = (Integer) emulatorsAndPorts[i + 1];
+ withEmulatorOnFixedPort(emulator, port);
+ }
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException("Emulators and ports must be specified alternating");
+ }
+
+ return this;
+ }
+
+ /**
+ * Finish the firebase configuration
+ *
+ * @return The primary builder
+ */
+ public Builder done() {
+ Builder.this.firebaseConfig = new FirebaseConfig(
+ hostingConfig,
+ storageConfig,
+ firestoreConfig,
+ functionsConfig,
+ services);
+ Builder.this.customFirebaseJson = null;
+
+ return Builder.this;
+ }
+ }
+ }
+
+ private final Map services;
+ private final boolean followStdOut;
+ private final boolean followStdErr;
+ private final Consumer afterStart;
+
+ /**
+ * Create the builder for the emulator container
+ *
+ * @return The builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates a new Firebase Emulator container
+ *
+ * @param emulatorConfig The generic configuration of the firebase emulators
+ */
+ public FirebaseEmulatorContainer(EmulatorConfig emulatorConfig) {
+ super(new FirebaseDockerBuilder(emulatorConfig).build());
+
+ this.services = emulatorConfig.firebaseConfig().services;
+ this.followStdOut = emulatorConfig.dockerConfig().followStdOut();
+ this.followStdErr = emulatorConfig.dockerConfig().followStdErr();
+ this.afterStart = emulatorConfig.dockerConfig().afterStart();
+
+ emulatorConfig.cliArguments().emulatorData().ifPresent(path -> {
+ // https://firebase.google.com/docs/emulator-suite/install_and_configure#export_and_import_emulator_data
+ // Mount the volume to the specified path
+ this.withFileSystemBind(path.toString(), EMULATOR_DATA_PATH, BindMode.READ_WRITE);
+ });
+
+ if (this.services.containsKey(Emulator.FIREBASE_HOSTING)) {
+ var hostingPath = emulatorConfig
+ .firebaseConfig()
+ .hostingConfig()
+ .hostingContentDir()
+ .map(Path::toString)
+ .orElse(new File(FirebaseJsonBuilder.FIREBASE_HOSTING_SUBPATH).getAbsolutePath());
+
+ // Mount volume for static hosting content
+ this.withFileSystemBind(hostingPath, containerHostingPath(emulatorConfig), BindMode.READ_ONLY);
+ }
+
+ if (this.services.containsKey(Emulator.CLOUD_FUNCTIONS)) {
+ var functionsPath = emulatorConfig
+ .firebaseConfig()
+ .functionsConfig()
+ .functionsPath()
+ .map(Path::toString)
+ .orElse(new File(FirebaseJsonBuilder.FIREBASE_FUNCTIONS_SUBPATH).getAbsolutePath());
+
+ // Mount volume for functions
+ this.withFileSystemBind(functionsPath, containerFunctionsPath(emulatorConfig), BindMode.READ_ONLY);
+ }
+ }
+
+ static String containerHostingPath(EmulatorConfig emulatorConfig) {
+ var hostingPath = emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir();
+ if (emulatorConfig.customFirebaseJson().isPresent()) {
+ var firebaseJsonDir = emulatorConfig.customFirebaseJson().get().getParent();
+ hostingPath = hostingPath.map(path -> path.subpath(firebaseJsonDir.getNameCount(), path.getNameCount()));
+ }
+
+ if (hostingPath.isPresent()) {
+ var path = hostingPath.get();
+ if (path.isAbsolute()) {
+ return FIREBASE_HOSTING_PATH;
+ } else {
+ return FIREBASE_ROOT + "/" + hostingPath.get();
+ }
+ } else {
+ return FIREBASE_HOSTING_PATH;
+ }
+ }
+
+ static String containerFunctionsPath(EmulatorConfig emulatorConfig) {
+ var functionsPath = emulatorConfig.firebaseConfig().functionsConfig().functionsPath();
+ if (emulatorConfig.customFirebaseJson().isPresent()) {
+ var firebaseJsonDir = emulatorConfig.customFirebaseJson().get().getParent();
+ functionsPath = functionsPath.map(path -> path.subpath(firebaseJsonDir.getNameCount(), path.getNameCount()));
+ }
+ return FIREBASE_ROOT + "/" + functionsPath
+ .map(Path::toString)
+ .orElse(FirebaseJsonBuilder.FIREBASE_FUNCTIONS_SUBPATH);
+ }
+
+ private static class FirebaseDockerBuilder {
+
+ private static final Map DOWNLOADABLE_EMULATORS = Map.of(
+ Emulator.REALTIME_DATABASE, "database",
+ Emulator.CLOUD_FIRESTORE, "firestore",
+ Emulator.PUB_SUB, "pubsub",
+ Emulator.CLOUD_STORAGE, "storage",
+ Emulator.EMULATOR_SUITE_UI, "ui");
+
+ private final ImageFromDockerfile result;
+
+ private final EmulatorConfig emulatorConfig;
+ private final Map devServices;
+
+ private DockerfileBuilder dockerBuilder;
+
+ public FirebaseDockerBuilder(EmulatorConfig emulatorConfig) {
+ this.devServices = emulatorConfig.firebaseConfig().services;
+ this.emulatorConfig = emulatorConfig;
+
+ this.result = new ImageFromDockerfile("localhost/testcontainers/firebase", false)
+ .withDockerfileFromBuilder(builder -> this.dockerBuilder = builder);
+ }
+
+ public ImageFromDockerfile build() {
+ this.validateConfiguration();
+ this.configureBaseImage();
+ this.initialSetup();
+ this.authenticateToFirebase();
+ this.setupJavaToolOptions();
+ this.setupUserAndGroup();
+ this.downloadEmulators();
+ this.addFirebaseJson();
+ this.includeFirestoreFiles();
+ this.includeStorageFiles();
+ this.setupDataImportExport();
+ this.setupHosting();
+ this.setupFunctions();
+ this.runExecutable();
+
+ return result;
+ }
+
+ private void validateConfiguration() {
+ if (isEmulatorEnabled(Emulator.AUTHENTICATION) && emulatorConfig.cliArguments().projectId().isEmpty()) {
+ throw new IllegalStateException("Can't create Firebase Auth emulator. Google Project id is required");
+ }
+
+ if (isEmulatorEnabled(Emulator.EMULATOR_SUITE_UI)) {
+ if (!isEmulatorEnabled(Emulator.EMULATOR_HUB)) {
+ LOGGER.info(
+ "Firebase Emulator UI is enabled, but no Hub port is specified. You will not be able to use the Hub API ");
+ }
+
+ if (!isEmulatorEnabled(Emulator.LOGGING)) {
+ LOGGER.info(
+ "Firebase Emulator UI is enabled, but no Logging port is specified. You will not be able to see the logging ");
+ }
+
+ if (isEmulatorEnabled(Emulator.CLOUD_FIRESTORE)) {
+ if (!isEmulatorEnabled(Emulator.CLOUD_FIRESTORE_WS)) {
+ LOGGER.warn("Firebase Firestore Emulator and Emulator UI are enabled but no Firestore Websocket " +
+ "port is specified. You will not be able to use the Firestore UI");
+ }
+ }
+ }
+
+ if (emulatorConfig.customFirebaseJson.isPresent()) {
+ var hostingDirIsAbsolute = emulatorConfig.firebaseConfig.hostingConfig.hostingContentDir
+ .map(Path::isAbsolute)
+ .orElse(false);
+
+ if (hostingDirIsAbsolute) {
+ throw new IllegalStateException(
+ "When using a custom firebase.json, the hosting path must be relative to the firebase.json file");
+ }
+
+ var firebasePath = emulatorConfig.customFirebaseJson.get().toAbsolutePath().getParent();
+
+ var hostingDirIsChildOfFirebaseJsonParent = emulatorConfig.firebaseConfig.hostingConfig.hostingContentDir
+ .map(Path::toAbsolutePath)
+ .map(h -> h.startsWith(firebasePath))
+ .orElse(true);
+
+ if (!hostingDirIsChildOfFirebaseJsonParent) {
+ throw new IllegalStateException(
+ "When using a custom firebase.json, the hosting path must be in the same subtree as the firebase.json file");
+ }
+ }
+
+ if (emulatorConfig.firebaseConfig.functionsConfig.functionsPath.isPresent()) {
+ var functionsDirIsAbsolute = emulatorConfig.firebaseConfig.functionsConfig.functionsPath
+ .map(Path::isAbsolute)
+ .orElse(false);
+
+ if (functionsDirIsAbsolute) {
+ throw new IllegalStateException("Functions path cannot be absolute");
+ }
+ }
+
+ // TODO: Validate if a custom firebase.json is defined, that the hosts are defined as 0.0.0.0
+ }
+
+ private void configureBaseImage() {
+ dockerBuilder.from(emulatorConfig.dockerConfig().imageName());
+ }
+
+ private void initialSetup() {
+ dockerBuilder
+ .run("apk --no-cache add openjdk17-jre bash curl openssl gettext nano nginx sudo && " +
+ "npm cache clean --force && " +
+ "npm i -g firebase-tools@" + emulatorConfig.firebaseVersion() + " && " +
+ "deluser nginx && delgroup abuild && delgroup ping && " +
+ "mkdir -p " + FIREBASE_ROOT + " && " +
+
+ "mkdir -p " + EMULATOR_DATA_PATH + " && " +
+ "mkdir -p " + EMULATOR_EXPORT_PATH + " && " +
+ "chmod 777 -R /srv/*");
+ }
+
+ private void downloadEmulators() {
+ var cmd = DOWNLOADABLE_EMULATORS
+ .entrySet()
+ .stream()
+ .map(e -> downloadEmulatorCommand(e.getKey(), e.getValue()))
+ .filter(Objects::nonNull)
+ .collect(Collectors.joining(" && "));
+
+ dockerBuilder.run(cmd);
+ }
+
+ private String downloadEmulatorCommand(Emulator emulator, String downloadId) {
+ if (isEmulatorEnabled(emulator)) {
+ return "firebase setup:emulators:" + downloadId;
+ } else {
+ return null;
+ }
+ }
+
+ private void authenticateToFirebase() {
+ emulatorConfig.cliArguments().token().ifPresent(
+ token -> dockerBuilder.env("FIREBASE_TOKEN", token));
+ }
+
+ private void setupJavaToolOptions() {
+ emulatorConfig.cliArguments().javaToolOptions().ifPresent(
+ toolOptions -> dockerBuilder.env("JAVA_TOOL_OPTIONS", toolOptions));
+ }
+
+ private void addFirebaseJson() {
+ dockerBuilder.workDir(FIREBASE_ROOT);
+
+ emulatorConfig.customFirebaseJson().ifPresentOrElse(
+ this::includeCustomFirebaseJson,
+ this::generateFirebaseJson);
+
+ this.dockerBuilder.add("firebase.json", FIREBASE_ROOT + "/firebase.json");
+ }
+
+ private void includeCustomFirebaseJson(Path customFilePath) {
+ this.result.withFileFromPath(
+ "firebase.json",
+ customFilePath);
+ }
+
+ private void includeFirestoreFiles() {
+ emulatorConfig.firebaseConfig().firestoreConfig.rulesFile.ifPresent(rulesFile -> {
+ this.dockerBuilder.add("firestore.rules", FIREBASE_ROOT + "/firestore.rules");
+ this.result.withFileFromPath("firestore.rules", rulesFile);
+ });
+
+ emulatorConfig.firebaseConfig().firestoreConfig.indexesFile.ifPresent(indexesFile -> {
+ this.dockerBuilder.add("firestore.indexes.json", FIREBASE_ROOT + "/firestore.indexes.json");
+ this.result.withFileFromPath("firestore.indexes.json", indexesFile);
+ });
+ }
+
+ private void includeStorageFiles() {
+ emulatorConfig.firebaseConfig().storageConfig.rulesFile.ifPresent(rulesFile -> {
+ this.dockerBuilder.add("storage.rules", FIREBASE_ROOT + "/storage.rules");
+ this.result.withFileFromPath("storage.rules", rulesFile);
+ });
+ }
+
+ private void generateFirebaseJson() {
+ var firebaseJsonBuilder = new FirebaseJsonBuilder(this.emulatorConfig);
+ String firebaseJson;
+ try {
+ firebaseJson = firebaseJsonBuilder.buildFirebaseConfig();
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to generate firebase.json file", e);
+ }
+
+ this.result.withFileFromString("firebase.json", firebaseJson);
+ }
+
+ private void setupDataImportExport() {
+ emulatorConfig.cliArguments().emulatorData().ifPresent(
+ emulator -> this.dockerBuilder.volume(EMULATOR_DATA_PATH));
+ }
+
+ private void setupHosting() {
+ // Specify public directory if hosting is enabled
+ if (emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir().isPresent()) {
+ this.dockerBuilder.run("mkdir -p " + containerHostingPath(emulatorConfig));
+ this.dockerBuilder.volume(containerHostingPath(emulatorConfig));
+ }
+ }
+
+ private void setupFunctions() {
+ if (emulatorConfig.firebaseConfig().functionsConfig().functionsPath.isPresent()) {
+ this.dockerBuilder.run("mkdir -p " + containerFunctionsPath(emulatorConfig));
+ this.dockerBuilder.volume(containerFunctionsPath(emulatorConfig));
+ }
+ }
+
+ private void setupUserAndGroup() {
+ var commands = new ArrayList();
+
+ emulatorConfig.dockerConfig.groupId().ifPresent(group -> commands.add("addgroup -g " + group + " runner"));
+
+ emulatorConfig.dockerConfig.userId().ifPresent(user -> {
+ var groupName = emulatorConfig.dockerConfig().groupId().map(i -> "runner").orElse("node");
+ commands.add("adduser -u " + user + " -G " + groupName + " -D -h /srv/firebase runner");
+ });
+
+ var group = dockerGroup();
+ var user = dockerUser();
+
+ commands.add("chown " + user + ":" + group + " -R /srv/*");
+
+ var runCmd = String.join(" && ", commands);
+
+ LOGGER.info("Running docker container as user/group: {}:{}", user, group);
+
+ dockerBuilder
+ .run(runCmd)
+ .user(user + ":" + group);
+ }
+
+ private int dockerUser() {
+ return emulatorConfig.dockerConfig().userId().orElse(1000);
+ }
+
+ private int dockerGroup() {
+ return emulatorConfig.dockerConfig().groupId().orElse(1000);
+ }
+
+ private void runExecutable() {
+ List arguments = new ArrayList<>();
+
+ arguments.add("emulators:start");
+
+ emulatorConfig.cliArguments().projectId()
+ .map(id -> "--project")
+ .ifPresent(arguments::add);
+
+ emulatorConfig.cliArguments().projectId()
+ .ifPresent(arguments::add);
+
+ if (emulatorConfig.cliArguments().debug) {
+ arguments.add("--debug");
+ }
+
+ if (emulatorConfig.cliArguments().importExport.isDoExport()) {
+ emulatorConfig
+ .cliArguments()
+ .emulatorData()
+ .map(path -> "--import")
+ .ifPresent(arguments::add);
+
+ /*
+ * We write the data to a subdirectory of the mount point. The firebase emulator tries to remove and
+ * recreate the mount-point directory, which will obviously fail. By using a subdirectory, export succeeds.
+ */
+ emulatorConfig
+ .cliArguments()
+ .emulatorData()
+ .map(path -> EMULATOR_EXPORT_PATH)
+ .ifPresent(arguments::add);
+ }
+
+ if (emulatorConfig.cliArguments().importExport.isDoExport()) {
+ emulatorConfig
+ .cliArguments()
+ .emulatorData()
+ .map(path -> "--export-on-exit")
+ .ifPresent(arguments::add);
+
+ /*
+ * We write the data to a subdirectory of the mount point. The firebase emulator tries to remove and
+ * recreate the mount-point directory, which will obviously fail. By using a subdirectory, export succeeds.
+ */
+ emulatorConfig
+ .cliArguments()
+ .emulatorData()
+ .map(path -> EMULATOR_EXPORT_PATH)
+ .ifPresent(arguments::add);
+ }
+
+ dockerBuilder.entryPoint(new String[] { "/usr/local/bin/firebase" });
+ dockerBuilder.cmd(arguments.toArray(new String[0]));
+ }
+
+ private boolean isEmulatorEnabled(Emulator emulator) {
+ return this.devServices.containsKey(emulator);
+ }
+ }
+
+ /**
+ * Override start to handle logging redirection
+ */
+ @Override
+ public void start() {
+ super.start();
+
+ if (followStdOut) {
+ followOutput(this::writeToStdOut, OutputFrame.OutputType.STDOUT);
+ }
+
+ if (followStdErr) {
+ followOutput(this::writeToStdErr, OutputFrame.OutputType.STDERR);
+ }
+
+ if (afterStart != null) {
+ afterStart.accept(this);
+ }
+ }
+
+ @Override
+ public void stop() {
+ /*
+ * We override the way test containers stops the container. By default, test containers will send a
+ * kill (SIGKILL) command instead of a stop (SIGTERM) command. This will kill the container instantly
+ * and prevent firebase from writing the "--export-on-exit" data to the mounted directory.
+ */
+ this.getDockerClient().stopContainerCmd(this.getContainerId()).exec();
+
+ super.stop();
+ }
+
+ /**
+ * Configures the Pub/Sub emulator container.
+ */
+ @Override
+ public void configure() {
+ super.configure();
+
+ services.keySet()
+ .forEach(emulator -> {
+ var exposedPort = services.get(emulator);
+ // Expose emulatorPort
+ if (exposedPort.isFixed()) {
+ addFixedExposedPort(exposedPort.fixedPort(), exposedPort.fixedPort());
+ } else {
+ addExposedPort(emulator.internalPort);
+ }
+ });
+
+ waitingFor(Wait.forLogMessage(".*Emulator Hub running at.*", 1));
+ }
+
+ /**
+ * Get the various endpoints for the emulators. The map values are in the form of a string "host:port".
+ *
+ * @return The emulator endpoints
+ */
+ public Map emulatorEndpoints() {
+ return services.keySet()
+ .stream()
+ .collect(Collectors.toMap(
+ e -> e,
+ this::getEmulatorEndpoint));
+ }
+
+ /**
+ * Return the TCP port an emulator is listening on.
+ *
+ * @param emulator The emulator
+ * @return The TC Port
+ */
+ public Integer emulatorPort(Emulator emulator) {
+ var exposedPort = services.get(emulator);
+ if (exposedPort.isFixed()) {
+ return exposedPort.fixedPort();
+ } else {
+ return getMappedPort(emulator.internalPort);
+ }
+ }
+
+ /**
+ * Get the ports on which the emulators are running.
+ *
+ * @return A map {@link Emulator} -> {@link Integer} indicating the TCP port the emulator is running on.
+ */
+ public Map emulatorPorts() {
+ return services.keySet()
+ .stream()
+ .collect(Collectors.toMap(
+ e -> e,
+ this::emulatorPort));
+ }
+
+ private void writeToStdOut(OutputFrame frame) {
+ writeOutputFrame(frame, Level.INFO);
+ }
+
+ private void writeToStdErr(OutputFrame frame) {
+ writeOutputFrame(frame, Level.ERROR);
+ }
+
+ private void writeOutputFrame(OutputFrame frame, Level level) {
+ LOGGER.atLevel(level).log(frame.getUtf8StringWithoutLineEnding());
+ }
+
+ private String getEmulatorEndpoint(Emulator emulator) {
+ return this.getHost() + ":" + emulatorPort(emulator);
+ }
+}
diff --git a/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseJsonBuilder.java b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseJsonBuilder.java
new file mode 100644
index 00000000..70057563
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseJsonBuilder.java
@@ -0,0 +1,228 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.json.*;
+
+/**
+ * This class is responsible to generate the Firebase.json file which controls the emulators.
+ */
+class FirebaseJsonBuilder {
+
+ private static final String ALL_IP = "0.0.0.0";
+ public static final String FIREBASE_HOSTING_SUBPATH = "public";
+ public static final String FIREBASE_FUNCTIONS_SUBPATH = "functions";
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+ private final FirebaseEmulatorContainer.EmulatorConfig emulatorConfig;
+ private final FirebaseConfig root;
+
+ public FirebaseJsonBuilder(FirebaseEmulatorContainer.EmulatorConfig emulatorConfig) {
+ this.emulatorConfig = emulatorConfig;
+ this.root = new FirebaseConfig();
+ }
+
+ public String buildFirebaseConfig() throws IOException {
+ generateFirebaseConfig();
+
+ StringWriter writer = new StringWriter();
+ objectMapper.writeValue(writer, root);
+ return writer.toString();
+ }
+
+ private void generateFirebaseConfig() {
+ // private Object database;
+ // private Object dataconnect;
+ configureEmulator();
+ // private ExtensionsConfig extensions;
+ configureFirestore();
+ configureFunctions();
+ configureHosting();
+ // private Remoteconfig remoteconfig;
+ configureStorage();
+ }
+
+ private void configureEmulator() {
+ var emulators = new Emulators();
+ root.setEmulators(emulators);
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.AUTHENTICATION, (port) -> {
+ var auth = new Auth();
+ emulators.setAuth(auth);
+ auth.setHost(ALL_IP);
+ auth.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE, (port) -> {
+ var database = new Database();
+ emulators.setDatabase(database);
+ database.setHost(ALL_IP);
+ database.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE, (port) -> {
+ var firestore = new Firestore();
+ emulators.setFirestore(firestore);
+ firestore.setHost(ALL_IP);
+ firestore.setPort(port);
+ withEmulator(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS, firestore::setWebsocketPort);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS, (port) -> {
+ var functions = new Functions();
+ emulators.setFunctions(functions);
+ functions.setHost(ALL_IP);
+ functions.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.EVENT_ARC, (port) -> {
+ var eventarc = new Eventarc();
+ emulators.setEventarc(eventarc);
+ eventarc.setHost(ALL_IP);
+ eventarc.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING, (port) -> {
+ var hosting = new Hosting();
+ emulators.setHosting(hosting);
+ hosting.setHost(ALL_IP);
+ hosting.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.EMULATOR_HUB, (port) -> {
+ var hub = new Hub();
+ emulators.setHub(hub);
+ hub.setHost(ALL_IP);
+ hub.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.LOGGING, (port) -> {
+ var logging = new Logging();
+ emulators.setLogging(logging);
+ logging.setHost(ALL_IP);
+ logging.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.PUB_SUB, (port) -> {
+ var pubSub = new Pubsub();
+ emulators.setPubsub(pubSub);
+ pubSub.setHost(ALL_IP);
+ pubSub.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE, (port) -> {
+ var storage = new Storage();
+ emulators.setStorage(storage);
+ storage.setHost(ALL_IP);
+ storage.setPort(port);
+ });
+
+ withEmulator(FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI, (port) -> {
+ var ui = new Ui();
+ emulators.setUi(ui);
+ ui.setHost(ALL_IP);
+ ui.setPort(port);
+ });
+
+ // Missing emulators
+ // private Apphosting apphosting;
+ // private Dataconnect dataconnect;
+ // private Extensions extensions;
+ // private Boolean singleProjectMode;
+ // private Tasks tasks;
+ }
+
+ private void withEmulator(FirebaseEmulatorContainer.Emulator emulator, Consumer handler) {
+ if (isEmulatorEnabled(emulator)) {
+ var exposedPort = emulatorConfig.firebaseConfig().services().get(emulator);
+ var port = Optional.ofNullable(exposedPort.fixedPort())
+ .orElse(emulator.internalPort);
+
+ handler.accept(port);
+ }
+ }
+
+ private void configureFirestore() {
+ if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE)) {
+ var firestore = new HashMap(); // Generated sources can't handle anyOf yet
+ root.setFirestore(firestore);
+
+ emulatorConfig.firebaseConfig().firestoreConfig().rulesFile().ifPresent(rules -> {
+ var rulesFile = fileRelativeToCustomJsonOrDefault(rules, "firestore.rules");
+ firestore.put("rules", rulesFile);
+ });
+
+ emulatorConfig.firebaseConfig().firestoreConfig().indexesFile().ifPresent(index -> {
+ var indexFile = fileRelativeToCustomJsonOrDefault(index, "firestore.indexes.json");
+ firestore.put("indexes", indexFile);
+ });
+ }
+ }
+
+ private void configureFunctions() {
+ if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS)) {
+ var functions = new HashMap();
+ root.setFunctions(functions);
+
+ var functionsPath = emulatorConfig
+ .firebaseConfig()
+ .functionsConfig()
+ .functionsPath()
+ .map(Path::toString)
+ .orElseThrow();
+
+ functions.put("source", functionsPath);
+ functions.put("ignores", new String[] { "node_modules" });
+ }
+ }
+
+ private void configureHosting() {
+ if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING)) {
+ var hosting = new HashMap();
+ root.setHosting(hosting);
+
+ var hostingPath = emulatorConfig
+ .firebaseConfig()
+ .hostingConfig()
+ .hostingContentDir()
+ .map(path -> path.isAbsolute() ? FIREBASE_HOSTING_SUBPATH : path.toString())
+ .orElseThrow();
+
+ hosting.put("public", hostingPath);
+ }
+ }
+
+ private void configureStorage() {
+ if (isEmulatorEnabled(FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE)) {
+ emulatorConfig.firebaseConfig().storageConfig().rulesFile().ifPresent(rules -> {
+ var storage = new HashMap(); // Generated sources can't handle anyOf yet
+ root.setStorage(storage);
+
+ var rulesFile = fileRelativeToCustomJsonOrDefault(rules, "storage.rules");
+ storage.put("rules", rulesFile);
+ });
+ }
+ }
+
+ private String fileRelativeToCustomJsonOrDefault(Path otherFile, String defaultFile) {
+ return emulatorConfig.customFirebaseJson()
+ .map(path -> relativePath(path, otherFile))
+ .orElse(defaultFile);
+ }
+
+ private String relativePath(Path firebaseJson, Path otherFile) {
+ return firebaseJson.getParent().relativize(otherFile).toString();
+ }
+
+ private boolean isEmulatorEnabled(FirebaseEmulatorContainer.Emulator emulator) {
+ return this.emulatorConfig.firebaseConfig().services().containsKey(emulator);
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/main/resources/META-INF/schema/firebase-config.json b/firebase-devservices/deployment/src/main/resources/META-INF/schema/firebase-config.json
new file mode 100644
index 00000000..02e77372
--- /dev/null
+++ b/firebase-devservices/deployment/src/main/resources/META-INF/schema/firebase-config.json
@@ -0,0 +1,2757 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "additionalProperties": false,
+ "definitions": {
+ "ExtensionsConfig": {
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "FrameworksBackendOptions": {
+ "additionalProperties": false,
+ "properties": {
+ "concurrency": {
+ "description": "Number of requests a function can serve at once.",
+ "type": "number"
+ },
+ "cors": {
+ "description": "If true, allows CORS on requests to this function.\nIf this is a `string` or `RegExp`, allows requests from domains that match the provided value.\nIf this is an `Array`, allows requests from domains matching at least one entry of the array.\nDefaults to true for {@link https.CallableFunction} and false otherwise.",
+ "type": [
+ "string",
+ "boolean"
+ ]
+ },
+ "cpu": {
+ "anyOf": [
+ {
+ "enum": [
+ "gcf_gen1"
+ ],
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "description": "Fractional number of CPUs to allocate to a function."
+ },
+ "enforceAppCheck": {
+ "description": "Determines whether Firebase AppCheck is enforced. Defaults to false.",
+ "type": "boolean"
+ },
+ "ingressSettings": {
+ "description": "Ingress settings which control where this function can be called from.",
+ "enum": [
+ "ALLOW_ALL",
+ "ALLOW_INTERNAL_AND_GCLB",
+ "ALLOW_INTERNAL_ONLY"
+ ],
+ "type": "string"
+ },
+ "invoker": {
+ "description": "Invoker to set access control on https functions.",
+ "enum": [
+ "public"
+ ],
+ "type": "string"
+ },
+ "labels": {
+ "$ref": "#/definitions/Record",
+ "description": "User labels to set on the function."
+ },
+ "maxInstances": {
+ "description": "Max number of instances to be running in parallel.",
+ "type": "number"
+ },
+ "memory": {
+ "description": "Amount of memory to allocate to a function.",
+ "enum": [
+ "128MiB",
+ "16GiB",
+ "1GiB",
+ "256MiB",
+ "2GiB",
+ "32GiB",
+ "4GiB",
+ "512MiB",
+ "8GiB"
+ ],
+ "type": "string"
+ },
+ "minInstances": {
+ "description": "Min number of actual instances to be running at a given time.",
+ "type": "number"
+ },
+ "omit": {
+ "description": "If true, do not deploy or emulate this function.",
+ "type": "boolean"
+ },
+ "preserveExternalChanges": {
+ "description": "Controls whether function configuration modified outside of function source is preserved. Defaults to false.",
+ "type": "boolean"
+ },
+ "region": {
+ "description": "HTTP functions can override global options and can specify multiple regions to deploy to.",
+ "type": "string"
+ },
+ "secrets": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "serviceAccount": {
+ "description": "Specific service account for the function to run as.",
+ "type": "string"
+ },
+ "timeoutSeconds": {
+ "description": "Timeout for the function in seconds, possible values are 0 to 540.\nHTTPS functions can specify a higher timeout.",
+ "type": "number"
+ },
+ "vpcConnector": {
+ "description": "Connect cloud function to specified VPC connector.",
+ "type": "string"
+ },
+ "vpcConnectorEgressSettings": {
+ "description": "Egress settings for VPC connector.",
+ "enum": [
+ "ALL_TRAFFIC",
+ "PRIVATE_RANGES_ONLY"
+ ],
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "Record": {
+ "additionalProperties": false,
+ "type": "object"
+ }
+ },
+ "properties": {
+ "$schema": {
+ "format": "uri",
+ "type": "string"
+ },
+ "database": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "rules"
+ ],
+ "type": "object"
+ },
+ {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "instance": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "instance",
+ "rules"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "instance": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "rules",
+ "target"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "dataconnect": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "source"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "emulators": {
+ "additionalProperties": false,
+ "properties": {
+ "apphosting": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "startCommandOverride": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "auth": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "database": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "dataconnect": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "postgresHost": {
+ "type": "string"
+ },
+ "postgresPort": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "eventarc": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "extensions": {
+ "properties": {
+ },
+ "type": "object"
+ },
+ "firestore": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ },
+ "websocketPort": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "functions": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "hosting": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "hub": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "logging": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "pubsub": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "singleProjectMode": {
+ "type": "boolean"
+ },
+ "storage": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "tasks": {
+ "additionalProperties": false,
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "ui": {
+ "additionalProperties": false,
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "host": {
+ "type": "string"
+ },
+ "port": {
+ "type": [
+ "integer"
+ ]
+ }
+ },
+ "type": "object"
+ }
+ },
+ "type": "object"
+ },
+ "extensions": {
+ "$ref": "#/definitions/ExtensionsConfig"
+ },
+ "firestore": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "database": {
+ "type": "string"
+ },
+ "indexes": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "database": {
+ "type": "string"
+ },
+ "indexes": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "database": {
+ "type": "string"
+ },
+ "indexes": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "database"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "functions": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "codebase": {
+ "type": "string"
+ },
+ "ignore": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "runtime": {
+ "enum": [
+ "nodejs10",
+ "nodejs12",
+ "nodejs14",
+ "nodejs16",
+ "nodejs18",
+ "nodejs20",
+ "nodejs22",
+ "nodejs6",
+ "nodejs8",
+ "python310",
+ "python311",
+ "python312"
+ ],
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "codebase": {
+ "type": "string"
+ },
+ "ignore": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "runtime": {
+ "enum": [
+ "nodejs10",
+ "nodejs12",
+ "nodejs14",
+ "nodejs16",
+ "nodejs18",
+ "nodejs20",
+ "nodejs22",
+ "nodejs6",
+ "nodejs8",
+ "python310",
+ "python311",
+ "python312"
+ ],
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "hosting": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "appAssociation": {
+ "enum": [
+ "AUTO",
+ "NONE"
+ ],
+ "type": "string"
+ },
+ "cleanUrls": {
+ "type": "boolean"
+ },
+ "frameworksBackend": {
+ "$ref": "#/definitions/FrameworksBackendOptions"
+ },
+ "headers": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "glob",
+ "headers"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "i18n": {
+ "additionalProperties": false,
+ "properties": {
+ "root": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "root"
+ ],
+ "type": "object"
+ },
+ "ignore": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "public": {
+ "type": "string"
+ },
+ "redirects": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "rewrites": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "glob",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "run",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "regex": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "regex",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "site": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ },
+ "trailingSlash": {
+ "type": "boolean"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "appAssociation": {
+ "enum": [
+ "AUTO",
+ "NONE"
+ ],
+ "type": "string"
+ },
+ "cleanUrls": {
+ "type": "boolean"
+ },
+ "frameworksBackend": {
+ "$ref": "#/definitions/FrameworksBackendOptions"
+ },
+ "headers": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "glob",
+ "headers"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "i18n": {
+ "additionalProperties": false,
+ "properties": {
+ "root": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "root"
+ ],
+ "type": "object"
+ },
+ "ignore": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "public": {
+ "type": "string"
+ },
+ "redirects": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "rewrites": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "glob",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "run",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "regex": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "regex",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "site": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ },
+ "trailingSlash": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "target"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "appAssociation": {
+ "enum": [
+ "AUTO",
+ "NONE"
+ ],
+ "type": "string"
+ },
+ "cleanUrls": {
+ "type": "boolean"
+ },
+ "frameworksBackend": {
+ "$ref": "#/definitions/FrameworksBackendOptions"
+ },
+ "headers": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "glob",
+ "headers"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "headers": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "key",
+ "value"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "headers",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "i18n": {
+ "additionalProperties": false,
+ "properties": {
+ "root": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "root"
+ ],
+ "type": "object"
+ },
+ "ignore": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "public": {
+ "type": "string"
+ },
+ "redirects": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "type": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "rewrites": {
+ "items": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "glob": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "glob": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "glob",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "glob": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "glob"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "run",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "source": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "source"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "destination": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "destination",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "type": "string"
+ },
+ "regex": {
+ "type": "string"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "function": {
+ "additionalProperties": false,
+ "properties": {
+ "functionId": {
+ "type": "string"
+ },
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "functionId"
+ ],
+ "type": "object"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "function",
+ "regex"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "regex": {
+ "type": "string"
+ },
+ "run": {
+ "additionalProperties": false,
+ "properties": {
+ "pinTag": {
+ "type": "boolean"
+ },
+ "region": {
+ "type": "string"
+ },
+ "serviceId": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "serviceId"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "regex",
+ "run"
+ ],
+ "type": "object"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dynamicLinks": {
+ "type": "boolean"
+ },
+ "regex": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dynamicLinks",
+ "regex"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ },
+ "site": {
+ "type": "string"
+ },
+ "source": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ },
+ "trailingSlash": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "site"
+ ],
+ "type": "object"
+ }
+ ]
+ },
+ "type": "array"
+ }
+ ]
+ },
+ "remoteconfig": {
+ "additionalProperties": false,
+ "properties": {
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "template": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "template"
+ ],
+ "type": "object"
+ },
+ "storage": {
+ "anyOf": [
+ {
+ "additionalProperties": false,
+ "properties": {
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "rules"
+ ],
+ "type": "object"
+ },
+ {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "bucket": {
+ "type": "string"
+ },
+ "postdeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "predeploy": {
+ "anyOf": [
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "type": "string"
+ }
+ ]
+ },
+ "rules": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "bucket",
+ "rules"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ]
+ }
+ },
+ "type": "object"
+}
diff --git a/firebase-devservices/deployment/src/test/firebase.json b/firebase-devservices/deployment/src/test/firebase.json
new file mode 100644
index 00000000..dc71457c
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/firebase.json
@@ -0,0 +1,40 @@
+{
+ "emulators": {
+ "firestore": {
+ "host": "0.0.0.0",
+ "port": 7002,
+ "websocketPort": 7003
+ },
+ "storage": {
+ "host": "0.0.0.0",
+ "port": 7005
+ },
+ "hosting": {
+ "host": "0.0.0.0",
+ "port": 7006
+ },
+ "functions": {
+ "host": "0.0.0.0",
+ "port": 7007
+ },
+ "ui": {
+ "host": "0.0.0.0",
+ "enabled": true,
+ "port": 7009
+ },
+ "singleProjectMode": true
+ },
+ "hosting": {
+ "public": "hosting"
+ },
+ "functions": {
+ "source": "functions"
+ },
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ }
+}
diff --git a/firebase-devservices/deployment/src/test/firestore.indexes.json b/firebase-devservices/deployment/src/test/firestore.indexes.json
new file mode 100644
index 00000000..28787432
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/firestore.indexes.json
@@ -0,0 +1,39 @@
+{
+ "indexes": [],
+ "fieldOverrides": [
+ {
+ "collectionGroup": "Aanmelding",
+ "fieldPath": "datum",
+ "ttl": false,
+ "indexes": [
+ {
+ "order": "ASCENDING",
+ "queryScope": "COLLECTION_GROUP"
+ }
+ ]
+ },
+ {
+ "collectionGroup": "events",
+ "fieldPath": "transactionId",
+ "ttl": false,
+ "indexes": [
+ {
+ "order": "ASCENDING",
+ "queryScope": "COLLECTION"
+ },
+ {
+ "order": "DESCENDING",
+ "queryScope": "COLLECTION"
+ },
+ {
+ "arrayConfig": "CONTAINS",
+ "queryScope": "COLLECTION"
+ },
+ {
+ "order": "ASCENDING",
+ "queryScope": "COLLECTION_GROUP"
+ }
+ ]
+ }
+ ]
+}
diff --git a/firebase-devservices/deployment/src/test/firestore.rules b/firebase-devservices/deployment/src/test/firestore.rules
new file mode 100644
index 00000000..5627b22e
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/firestore.rules
@@ -0,0 +1,9 @@
+rules_version = '2';
+service cloud.firestore {
+ match /databases/{database}/documents {
+ match /data/{document} {
+ allow read: if request.auth != null && request.auth.uid == resource.data.ownerId;
+ allow write: if request.auth != null && request.auth.uid == resource.data.ownerId;
+ }
+ }
+}
\ No newline at end of file
diff --git a/firebase-devservices/deployment/src/test/functions/.gitignore b/firebase-devservices/deployment/src/test/functions/.gitignore
new file mode 100644
index 00000000..b512c09d
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/functions/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/firebase-devservices/deployment/src/test/functions/index.js b/firebase-devservices/deployment/src/test/functions/index.js
new file mode 100644
index 00000000..890feb44
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/functions/index.js
@@ -0,0 +1,8 @@
+// The Cloud Functions for Firebase SDK to create Cloud Functions and triggers.
+const {logger} = require("firebase-functions");
+const {onRequest} = require("firebase-functions/v2/https");
+
+exports.helloworld = onRequest(async (req, res) => {
+ logger.log("Received hello world request");
+ res.send("Hello world");
+});
diff --git a/firebase-devservices/deployment/src/test/functions/package-lock.json b/firebase-devservices/deployment/src/test/functions/package-lock.json
new file mode 100644
index 00000000..fc4671b0
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/functions/package-lock.json
@@ -0,0 +1,2694 @@
+{
+ "name": "functions",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "functions",
+ "version": "1.0.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "firebase-admin": "^13.0.1",
+ "firebase-functions": "^6.1.2"
+ },
+ "engines": {
+ "node": "20"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.0.tgz",
+ "integrity": "sha512-yHmUtGwEbW6HsKpPqT140/L6GpHtquHogRLgtanJFep3UAfDkE0fQfC49U+F9irCAoJVlv3M7VSp4rrtO4LnfA==",
+ "license": "MIT"
+ },
+ "node_modules/@firebase/app-check-interop-types": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz",
+ "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/app-types": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
+ "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/auth-interop-types": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz",
+ "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@firebase/component": {
+ "version": "0.6.11",
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz",
+ "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/util": "1.10.2",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/database": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz",
+ "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.3",
+ "@firebase/auth-interop-types": "0.2.4",
+ "@firebase/component": "0.6.11",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.10.2",
+ "faye-websocket": "0.11.4",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/database-compat": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz",
+ "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/component": "0.6.11",
+ "@firebase/database": "1.0.10",
+ "@firebase/database-types": "1.0.7",
+ "@firebase/logger": "0.4.4",
+ "@firebase/util": "1.10.2",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/database-types": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz",
+ "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@firebase/app-types": "0.9.3",
+ "@firebase/util": "1.10.2"
+ }
+ },
+ "node_modules/@firebase/logger": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz",
+ "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@firebase/util": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz",
+ "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@google-cloud/firestore": {
+ "version": "7.11.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz",
+ "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0",
+ "fast-deep-equal": "^3.1.1",
+ "functional-red-black-tree": "^1.0.1",
+ "google-gax": "^4.3.3",
+ "protobufjs": "^7.2.6"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@google-cloud/paginator": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz",
+ "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "arrify": "^2.0.0",
+ "extend": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@google-cloud/projectify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz",
+ "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@google-cloud/promisify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz",
+ "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@google-cloud/storage": {
+ "version": "7.14.0",
+ "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.14.0.tgz",
+ "integrity": "sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@google-cloud/paginator": "^5.0.0",
+ "@google-cloud/projectify": "^4.0.0",
+ "@google-cloud/promisify": "^4.0.0",
+ "abort-controller": "^3.0.0",
+ "async-retry": "^1.3.3",
+ "duplexify": "^4.1.3",
+ "fast-xml-parser": "^4.4.1",
+ "gaxios": "^6.0.2",
+ "google-auth-library": "^9.6.3",
+ "html-entities": "^2.5.2",
+ "mime": "^3.0.0",
+ "p-limit": "^3.0.1",
+ "retry-request": "^7.0.0",
+ "teeny-request": "^9.0.0",
+ "uuid": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@google-cloud/storage/node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.12.4",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.4.tgz",
+ "integrity": "sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.13",
+ "@js-sdsl/ordered-map": "^4.4.2"
+ },
+ "engines": {
+ "node": ">=12.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.13",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
+ "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@js-sdsl/ordered-map": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
+ "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/js-sdsl"
+ }
+ },
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
+ "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.5",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
+ "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/caseless": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz",
+ "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.17",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
+ "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
+ "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^4.17.33",
+ "@types/qs": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "4.19.6",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
+ "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
+ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/jsonwebtoken": {
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
+ "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/long": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
+ "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@types/mime": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.10.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
+ "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.20.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.9.17",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
+ "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/request": {
+ "version": "2.48.12",
+ "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz",
+ "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/caseless": "*",
+ "@types/node": "*",
+ "@types/tough-cookie": "*",
+ "form-data": "^2.5.0"
+ }
+ },
+ "node_modules/@types/send": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
+ "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/mime": "^1",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.7",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
+ "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
+ "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/arrify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async-retry": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz",
+ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "retry": "0.13.1"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bignumber.js": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz",
+ "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
+ "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz",
+ "integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "get-intrinsic": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
+ "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/duplexify": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
+ "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "end-of-stream": "^1.4.1",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1",
+ "stream-shift": "^1.0.2"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz",
+ "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
+ "node_modules/farmhash-modern": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz",
+ "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/fast-xml-parser": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz",
+ "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/naturalintelligence"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "strnum": "^1.0.5"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/firebase-admin": {
+ "version": "13.0.1",
+ "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.0.1.tgz",
+ "integrity": "sha512-sKQ/Yw8o/WdC9qTKvuLMBjTbdcBISIXW4+M9PXk0bNjxEbZf1Er7EVq47eRb5+bnKof10xlns6zAIbj4tmSexg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@fastify/busboy": "^3.0.0",
+ "@firebase/database-compat": "^2.0.0",
+ "@firebase/database-types": "^1.0.6",
+ "@types/node": "^22.8.7",
+ "farmhash-modern": "^1.1.0",
+ "google-auth-library": "^9.14.2",
+ "jsonwebtoken": "^9.0.0",
+ "jwks-rsa": "^3.1.0",
+ "node-forge": "^1.3.1",
+ "uuid": "^11.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@google-cloud/firestore": "^7.10.0",
+ "@google-cloud/storage": "^7.14.0"
+ }
+ },
+ "node_modules/firebase-functions": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-6.1.2.tgz",
+ "integrity": "sha512-1ZKLLOs4YhpzfWOZo0wsqNBusy9113GUfRs89BF6yOlmkxlcJxdJzZEs/ygWeXVJKquZhW2K1Gm10Gir4RJxGQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.5",
+ "@types/express": "^4.17.21",
+ "cors": "^2.8.5",
+ "express": "^4.21.0",
+ "protobufjs": "^7.2.2"
+ },
+ "bin": {
+ "firebase-functions": "lib/bin/firebase-functions.js"
+ },
+ "engines": {
+ "node": ">=14.10.0"
+ },
+ "peerDependencies": {
+ "firebase-admin": "^11.10.0 || ^12.0.0 || ^13.0.0"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz",
+ "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.6",
+ "mime-types": "^2.1.12",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.12"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/gaxios": {
+ "version": "6.7.1",
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+ "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "extend": "^3.0.2",
+ "https-proxy-agent": "^7.0.1",
+ "is-stream": "^2.0.0",
+ "node-fetch": "^2.6.9",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/gaxios/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/gcp-metadata": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz",
+ "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "json-bigint": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "optional": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz",
+ "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "dunder-proto": "^1.0.0",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "function-bind": "^1.1.2",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/google-auth-library": {
+ "version": "9.15.0",
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz",
+ "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "base64-js": "^1.3.0",
+ "ecdsa-sig-formatter": "^1.0.11",
+ "gaxios": "^6.1.1",
+ "gcp-metadata": "^6.1.0",
+ "gtoken": "^7.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-gax": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz",
+ "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@grpc/grpc-js": "^1.10.9",
+ "@grpc/proto-loader": "^0.7.13",
+ "@types/long": "^4.0.0",
+ "abort-controller": "^3.0.0",
+ "duplexify": "^4.0.0",
+ "google-auth-library": "^9.3.0",
+ "node-fetch": "^2.7.0",
+ "object-hash": "^3.0.0",
+ "proto3-json-serializer": "^2.0.2",
+ "protobufjs": "^7.3.2",
+ "retry-request": "^7.0.0",
+ "uuid": "^9.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/google-gax/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gtoken": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+ "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+ "license": "MIT",
+ "dependencies": {
+ "gaxios": "^6.0.0",
+ "jws": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/html-entities": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
+ "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/mdevils"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/mdevils"
+ }
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-parser-js": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==",
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jose": {
+ "version": "4.15.9",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
+ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
+ "node_modules/json-bigint": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bignumber.js": "^9.0.0"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jwa": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
+ "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jwks-rsa": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz",
+ "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/express": "^4.17.17",
+ "@types/jsonwebtoken": "^9.0.2",
+ "debug": "^4.3.4",
+ "jose": "^4.14.6",
+ "limiter": "^1.1.5",
+ "lru-memoizer": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/jwks-rsa/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jwks-rsa/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/jws": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.0",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/limiter": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz",
+ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/lru-memoizer": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz",
+ "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash.clonedeep": "^4.5.0",
+ "lru-cache": "6.0.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz",
+ "integrity": "sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-forge": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
+ "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+ "license": "(BSD-3-Clause OR GPL-2.0)",
+ "engines": {
+ "node": ">= 6.13.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
+ "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "optional": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proto3-json-serializer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
+ "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "protobufjs": "^7.2.5"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/protobufjs": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
+ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/retry-request": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz",
+ "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@types/request": "^2.48.8",
+ "extend": "^3.0.2",
+ "teeny-request": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/stream-events": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz",
+ "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "stubs": "^3.0.0"
+ }
+ },
+ "node_modules/stream-shift": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
+ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strnum": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz",
+ "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/stubs": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
+ "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/teeny-request": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz",
+ "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "node-fetch": "^2.6.9",
+ "stream-events": "^1.0.5",
+ "uuid": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/teeny-request/node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/teeny-request/node_modules/debug": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
+ "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/teeny-request/node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/teeny-request/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/teeny-request/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.20.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
+ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
+ "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC",
+ "optional": true
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "license": "ISC",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "license": "ISC",
+ "optional": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/firebase-devservices/deployment/src/test/functions/package.json b/firebase-devservices/deployment/src/test/functions/package.json
new file mode 100644
index 00000000..a43fa9cc
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/functions/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "functions",
+ "version": "1.0.0",
+ "description": "Test functions for TestContainers Firebase",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "author": "Jeroen Benckhuijsen (jeroen.benckhuijsen@group9.nl)",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "20"
+ },
+ "dependencies": {
+ "firebase-admin": "^13.0.1",
+ "firebase-functions": "^6.1.2"
+ }
+}
diff --git a/firebase-devservices/deployment/src/test/hosting/test.me b/firebase-devservices/deployment/src/test/hosting/test.me
new file mode 100644
index 00000000..003eae77
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/hosting/test.me
@@ -0,0 +1 @@
+This is a test file for hosting
\ No newline at end of file
diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java
new file mode 100644
index 00000000..49397aca
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/FirebaseEmulatorConfigBuilderTest.java
@@ -0,0 +1,213 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers.FirebaseEmulatorContainer;
+
+class FirebaseEmulatorConfigBuilderTest {
+
+ private FirebaseEmulatorConfigBuilder configBuilder;
+
+ @BeforeEach
+ void setUp() {
+ var projectConfig = new TestProjectConfig(
+ Optional.of("my-project-id"));
+ var config = new TestFirebaseDevServiceConfig(
+ new TestFirebase(
+ true,
+ new TestFirebaseEmulator(
+ "11.0.0",
+ new TestDocker(
+ "node:21-alpine",
+ Optional.of(1001),
+ Optional.of(1002),
+ Optional.empty(),
+ Optional.empty(),
+ Optional.of(false),
+ Optional.of(true)),
+ new TestCli(
+ Optional.of("MY_TOKEN"),
+ Optional.of("-Xmx"),
+ Optional.of("data"),
+ Optional.of(FirebaseEmulatorContainer.ImportExport.EXPORT_ONLY),
+ Optional.of(true)),
+ Optional.empty(),
+ new TestUI(
+ true,
+ Optional.of(6000),
+ Optional.of(6001),
+ Optional.of(6002))),
+ new TestGenericDevService(true, Optional.of(6003)),
+ new TestHosting(
+ true,
+ Optional.of(6004),
+ Optional.of("public")),
+ new TestGenericDevService(
+ false,
+ Optional.of(6005)),
+ new TestFirestoreDevService(
+ true,
+ Optional.of(6006),
+ Optional.of(6007),
+ Optional.of("firestore.rules"),
+ Optional.of("firestore.indexes.json"))),
+ new TestGenericDevService(
+ true,
+ Optional.of(6008)),
+ new TestGenericDevService(
+ true,
+ Optional.of(6009)),
+ new TestStorageDevService(
+ true,
+ Optional.empty(),
+ Optional.of("storage.rules")));
+ configBuilder = new FirebaseEmulatorConfigBuilder(projectConfig, config);
+ }
+
+ @Test
+ void testBuild() {
+ FirebaseEmulatorContainer.EmulatorConfig emulatorConfig = configBuilder.buildConfig();
+
+ assertNotNull(emulatorConfig);
+
+ assertEquals("node:21-alpine", emulatorConfig.dockerConfig().imageName());
+ assertEquals(1001, emulatorConfig.dockerConfig().userId().orElse(null));
+ assertEquals(1002, emulatorConfig.dockerConfig().groupId().orElse(null));
+ assertFalse(emulatorConfig.dockerConfig().followStdOut());
+ assertTrue(emulatorConfig.dockerConfig().followStdErr());
+
+ assertEquals("11.0.0", emulatorConfig.firebaseVersion());
+
+ assertEquals("my-project-id", emulatorConfig.cliArguments().projectId().orElse(null));
+ assertEquals("MY_TOKEN", emulatorConfig.cliArguments().token().orElse(null));
+ assertEquals("-Xmx", emulatorConfig.cliArguments().javaToolOptions().orElse(null));
+ assertPathEndsWith("data", emulatorConfig.cliArguments().emulatorData().orElse(null));
+ assertEquals(FirebaseEmulatorContainer.ImportExport.EXPORT_ONLY, emulatorConfig.cliArguments().importExport());
+ assertTrue(emulatorConfig.cliArguments().debug());
+
+ assertTrue(emulatorConfig.customFirebaseJson().isEmpty());
+
+ assertPathEndsWith("public", emulatorConfig.firebaseConfig().hostingConfig().hostingContentDir().orElse(null));
+ assertPathEndsWith("storage.rules", emulatorConfig.firebaseConfig().storageConfig().rulesFile().orElse(null));
+ assertPathEndsWith("firestore.rules", emulatorConfig.firebaseConfig().firestoreConfig().rulesFile().orElse(null));
+ assertPathEndsWith("firestore.indexes.json",
+ emulatorConfig.firebaseConfig().firestoreConfig().indexesFile().orElse(null));
+
+ }
+
+ private void assertPathEndsWith(String expected, Path path) {
+ assertNotNull(path);
+ assertTrue(path.toString().endsWith(expected));
+ }
+
+ @Test
+ void testExposedEmulators() {
+ FirebaseEmulatorContainer.EmulatorConfig emulatorConfig = configBuilder.buildConfig();
+
+ Map exposedPorts = emulatorConfig
+ .firebaseConfig().services();
+
+ assertEquals(10, exposedPorts.size());
+ assertEquals(6000, exposedPorts.get(FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI).fixedPort());
+ assertEquals(6001, exposedPorts.get(FirebaseEmulatorContainer.Emulator.LOGGING).fixedPort());
+ assertEquals(6002, exposedPorts.get(FirebaseEmulatorContainer.Emulator.EMULATOR_HUB).fixedPort());
+ assertEquals(6003, exposedPorts.get(FirebaseEmulatorContainer.Emulator.AUTHENTICATION).fixedPort());
+ assertEquals(6004, exposedPorts.get(FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING).fixedPort());
+ assertEquals(6006, exposedPorts.get(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE).fixedPort());
+ assertEquals(6007, exposedPorts.get(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS).fixedPort());
+ assertEquals(6008, exposedPorts.get(FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS).fixedPort());
+ assertEquals(6009, exposedPorts.get(FirebaseEmulatorContainer.Emulator.PUB_SUB).fixedPort());
+ assertNull(exposedPorts.get(FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE).fixedPort());
+
+ assertNull(exposedPorts.get(FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE));
+ }
+
+ // Record implementations for interfaces
+ record TestProjectConfig(
+ Optional projectId) implements FirebaseDevServiceProjectConfig {
+ }
+
+ record TestFirebaseDevServiceConfig(
+ FirebaseDevServiceConfig.Firebase firebase,
+ FirebaseDevServiceConfig.GenericDevService functions,
+ FirebaseDevServiceConfig.GenericDevService pubsub,
+ FirebaseDevServiceConfig.StorageDevService storage) implements FirebaseDevServiceConfig {
+ }
+
+ record TestFirebase(
+ boolean preferFirebaseDevServices,
+ Emulator emulator,
+ FirebaseDevServiceConfig.GenericDevService auth,
+ FirebaseDevServiceConfig.Firebase.HostingDevService hosting,
+ FirebaseDevServiceConfig.GenericDevService database,
+ FirebaseDevServiceConfig.Firebase.FirestoreDevService firestore) implements FirebaseDevServiceConfig.Firebase {
+
+ }
+
+ record TestFirebaseEmulator(
+ String firebaseVersion,
+ FirebaseDevServiceConfig.Firebase.Emulator.Docker docker,
+ FirebaseDevServiceConfig.Firebase.Emulator.Cli cli,
+ Optional customFirebaseJson,
+ UI ui) implements FirebaseDevServiceConfig.Firebase.Emulator {
+ }
+
+ record TestDocker(
+ String imageName,
+ Optional dockerUser,
+ Optional dockerGroup,
+ Optional dockerUserEnv,
+ Optional dockerGroupEnv,
+ Optional followStdOut,
+ Optional followStdErr) implements FirebaseDevServiceConfig.Firebase.Emulator.Docker {
+ }
+
+ record TestCli(
+ Optional token,
+ Optional javaToolOptions,
+ Optional emulatorData,
+ Optional importExport,
+ Optional debug) implements FirebaseDevServiceConfig.Firebase.Emulator.Cli {
+ }
+
+ record TestUI(
+ boolean enabled,
+ Optional emulatorPort,
+ Optional loggingPort,
+ Optional hubPort) implements FirebaseDevServiceConfig.Firebase.Emulator.UI {
+ }
+
+ record TestFirestoreDevService(
+ boolean enabled,
+ Optional emulatorPort,
+ Optional websocketPort,
+ Optional rulesFile,
+ Optional indexesFile) implements FirebaseDevServiceConfig.Firebase.FirestoreDevService {
+ }
+
+ record TestHosting(
+ boolean enabled,
+ Optional emulatorPort,
+ Optional hostingPath) implements FirebaseDevServiceConfig.Firebase.HostingDevService {
+ }
+
+ record TestStorageDevService(
+ boolean enabled,
+ Optional emulatorPort,
+ Optional rulesFile) implements FirebaseDevServiceConfig.StorageDevService {
+
+ }
+
+ record TestGenericDevService(
+ boolean enabled,
+ Optional emulatorPort) implements FirebaseDevServiceConfig.GenericDevService {
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java
new file mode 100644
index 00000000..77312a87
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerCustomConfigTest.java
@@ -0,0 +1,88 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+
+import org.junit.jupiter.api.Test;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+@Testcontainers
+public class FirebaseEmulatorContainerCustomConfigTest {
+
+ private static final File tempEmulatorDataDir;
+
+ static {
+ try {
+ // Create a temporary directory for emulator data
+ tempEmulatorDataDir = new File("target/firebase-emulator-container-data");
+ tempEmulatorDataDir.mkdirs();
+ var testContainer = new TestableFirebaseEmulatorContainer("FirebaseEmulatorContainerCustomConfigTest");
+ firebaseContainer = testContainer.testBuilder()
+ .withCliArguments()
+ .withEmulatorData(tempEmulatorDataDir.toPath())
+ .done()
+ .readFromFirebaseJson(new File("src/test/firebase.json").toPath())
+ .build();
+
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Container
+ private static final FirebaseEmulatorContainer firebaseContainer;
+
+ @Test
+ public void testFirestoreRulesAndIndexes() throws InterruptedException, IOException {
+ // Verify the firebase.json file exists in the container
+ String firebaseJsonCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/firebase.json").getStdout();
+ assertTrue(firebaseJsonCheck.contains("\"emulators\""), "Expected firebase.json to be present in the container");
+
+ // Verify the firestore.rules file exists in the container
+ String firestoreRulesCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/firestore.rules").getStdout();
+ assertTrue(firestoreRulesCheck.contains("service cloud.firestore"),
+ "Expected firestore.rules to be present in the container");
+ }
+
+ @Test
+ public void testStorageRules() throws IOException, InterruptedException {
+ // Verify the storage.rules file exists in the container
+ String storageRulesCheck = firebaseContainer.execInContainer("cat", "/srv/firebase/storage.rules").getStdout();
+ assertTrue(storageRulesCheck.contains("service firebase.storage"),
+ "Expected storage.rules to be present in the container");
+ }
+
+ @Test
+ public void testHosting() throws IOException, InterruptedException, URISyntaxException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ var request = HttpRequest.newBuilder()
+ .GET()
+ .uri(new URI("http://localhost:7006/test.me"))
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ var body = response.body();
+ assertEquals("This is a test file for hosting", body);
+ }
+
+ @Test
+ public void testFunctions() throws IOException, InterruptedException, URISyntaxException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ var request = HttpRequest.newBuilder()
+ .GET()
+ .uri(new URI("http://localhost:7007/demo-test-project/us-central1/helloworld"))
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ var body = response.body();
+ assertEquals("Hello world", body);
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java
new file mode 100644
index 00000000..122cbba3
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/FirebaseEmulatorContainerIntegrationTest.java
@@ -0,0 +1,362 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.grpc.GrpcTransportChannel;
+import com.google.api.gax.rpc.FixedTransportChannelProvider;
+import com.google.cloud.NoCredentials;
+import com.google.cloud.firestore.*;
+import com.google.cloud.pubsub.v1.Publisher;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
+import com.google.cloud.pubsub.v1.TopicAdminSettings;
+import com.google.cloud.storage.*;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.auth.FirebaseAuth;
+import com.google.firebase.auth.FirebaseAuthException;
+import com.google.firebase.auth.UserRecord;
+import com.google.firebase.database.*;
+import com.google.firebase.internal.EmulatorCredentials;
+import com.google.firebase.internal.FirebaseProcessEnvironment;
+import com.google.protobuf.ByteString;
+import com.google.pubsub.v1.PubsubMessage;
+
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+
+@Testcontainers
+public class FirebaseEmulatorContainerIntegrationTest {
+
+ private static final File tempDataParent;
+ private static final File tempEmulatorDataDir;
+ private static final File tempHostingContentDir;
+
+ static {
+ try {
+ tempDataParent = new File("target/firebase-emulator-it");
+
+ // Create a temporary directory for emulator data
+ tempEmulatorDataDir = new File(tempDataParent, "firebase-emulator-data");
+ tempEmulatorDataDir.mkdirs();
+ tempHostingContentDir = new File(tempDataParent, "firebase-hosting-content");
+ tempHostingContentDir.mkdirs();
+
+ // Create a static HTML file in the hosting directory
+ File indexFile = new File(tempHostingContentDir, "index.html");
+ try (FileWriter writer = new FileWriter(indexFile, Charset.defaultCharset())) {
+ writer.write("Hello, Firebase Hosting!
");
+ }
+
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ private static final TestableFirebaseEmulatorContainer testContainer = new TestableFirebaseEmulatorContainer(
+ "FirebaseEmulatorContainerIntegrationTest",
+ FirebaseEmulatorContainerIntegrationTest::customizeFirebaseOptions);
+
+ private static final FirebaseEmulatorContainer firebaseContainer = testContainer.testBuilder()
+ .withCliArguments()
+ .withEmulatorData(tempEmulatorDataDir.toPath())
+ .done()
+ .withFirebaseConfig()
+ .withHostingPath(tempHostingContentDir.toPath())
+ .withFunctionsFromPath(new File("src/test/functions").toPath())
+ .withEmulatorsOnPorts(
+ FirebaseEmulatorContainer.Emulator.AUTHENTICATION, 6000,
+ FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE, 6001,
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE, 6002,
+ FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE_WS, 6003,
+ FirebaseEmulatorContainer.Emulator.PUB_SUB, 6004,
+ FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE, 6005,
+ FirebaseEmulatorContainer.Emulator.FIREBASE_HOSTING, 6006,
+ FirebaseEmulatorContainer.Emulator.CLOUD_FUNCTIONS, 6007,
+ // Emulator.EVENT_ARC, 6008,
+ FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI, 6009,
+ FirebaseEmulatorContainer.Emulator.EMULATOR_HUB, 6010,
+ FirebaseEmulatorContainer.Emulator.LOGGING, 6011)
+ .done()
+ .build();
+
+ private static void customizeFirebaseOptions(FirebaseOptions.Builder builder) {
+ var emulatorHost = firebaseContainer.getHost();
+ var dbPort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.REALTIME_DATABASE);
+
+ builder.setDatabaseUrl("http://" + emulatorHost + ":" + dbPort + "?ns=demo-test-project");
+ }
+
+ @BeforeAll
+ public static void setup() {
+ firebaseContainer.start();
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ firebaseContainer.stop();
+
+ validateEmulatorDataWritten();
+
+ // Recursively delete the contents of the directories and then delete the directories
+ deleteDirectoryRecursively(tempDataParent);
+ }
+
+ // Helper method to recursively delete all files and directories
+ private static void deleteDirectoryRecursively(File directory) {
+ if (directory.exists()) {
+ File[] files = directory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteDirectoryRecursively(file);
+ } else {
+ assertTrue(file.delete());
+ }
+ }
+ }
+ assertTrue(directory.delete());
+ }
+ }
+
+ private static void validateEmulatorDataWritten() {
+ var emulatorDataDir = new File(tempEmulatorDataDir, "emulator-data");
+ assertTrue(emulatorDataDir.exists());
+ assertTrue(emulatorDataDir.isDirectory());
+ assertTrue(emulatorDataDir.canRead());
+ assertTrue(emulatorDataDir.canWrite());
+ assertTrue(emulatorDataDir.canExecute());
+
+ // Verify that files were written to the emulator data directory
+ File[] files = emulatorDataDir.listFiles();
+ assertNotNull(files);
+ assertTrue(files.length > 0, "Expected files to be present in the emulator data directory");
+ }
+
+ @Test
+ public void testFirebaseAuthenticationEmulatorConnection() throws FirebaseAuthException {
+ // Retrieve the host and port for the Authentication emulator
+ int authPort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.AUTHENTICATION);
+
+ // Set the environment variable for the Firebase Authentication emulator
+ FirebaseProcessEnvironment.setenv("FIREBASE_AUTH_EMULATOR_HOST", firebaseContainer.getHost() + ":" + authPort);
+
+ // Initialize FirebaseOptions without setting the auth emulator host directly
+ FirebaseAuth auth = FirebaseAuth.getInstance(testContainer.getApp());
+
+ // Create a test user and verify it
+ UserRecord.CreateRequest request = new UserRecord.CreateRequest()
+ .setEmail("user@example.com")
+ .setPassword("password");
+ UserRecord userRecord = auth.createUser(request);
+
+ assertNotNull(userRecord);
+ assertEquals("user@example.com", userRecord.getEmail());
+
+ // Clean up by deleting the test user
+ auth.deleteUser(userRecord.getUid());
+ }
+
+ @Test
+ public void testFirestoreEmulatorConnection() throws Exception {
+ int firestorePort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.CLOUD_FIRESTORE);
+
+ FirestoreOptions options = FirestoreOptions.newBuilder()
+ .setProjectId("demo-test-project")
+ .setEmulatorHost(firebaseContainer.getHost() + ":" + firestorePort)
+ .setCredentials(new EmulatorCredentials())
+ .build();
+
+ try (Firestore firestore = options.getService()) {
+ DocumentReference docRef = firestore.collection("testCollection").document("testDoc");
+ ApiFuture result = docRef.set(Map.of("field", "value"));
+
+ assertNotNull(result.get());
+ DocumentSnapshot snapshot = docRef.get().get();
+ assertEquals("value", snapshot.getString("field"));
+ }
+ }
+
+ @Test
+ public void testRealtimeDatabaseEmulatorConnection() throws ExecutionException, InterruptedException {
+ DatabaseReference ref = FirebaseDatabase.getInstance(testContainer.getApp()).getReference("testData");
+
+ // Write data to the database
+ ref.setValueAsync("testValue").get();
+
+ // Set up a listener and latch for asynchronous reading
+ CountDownLatch latch = new CountDownLatch(1);
+ final String[] value = { null };
+
+ ref.addListenerForSingleValueEvent(new ValueEventListener() {
+ @Override
+ public void onDataChange(DataSnapshot snapshot) {
+ value[0] = snapshot.getValue(String.class);
+ latch.countDown();
+ }
+
+ @Override
+ public void onCancelled(DatabaseError error) {
+ latch.countDown();
+ }
+ });
+
+ // Wait for the listener to retrieve data
+ assertTrue(latch.await(5, TimeUnit.SECONDS));
+ assertEquals("testValue", value[0], "Expected to retrieve 'testValue' from Realtime Database");
+ }
+
+ @Test
+ public void testPubSubEmulatorConnection() throws Exception {
+ // Retrieve the host and port for the Pub/Sub emulator
+ int pubSubPort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.PUB_SUB);
+
+ // Set up a gRPC channel to the Pub/Sub emulator
+ ManagedChannel channel = ManagedChannelBuilder.forAddress(firebaseContainer.getHost(), pubSubPort)
+ .usePlaintext()
+ .build();
+
+ // Set the channel provider for Pub/Sub client
+ FixedTransportChannelProvider channelProvider = FixedTransportChannelProvider
+ .create(GrpcTransportChannel.create(channel));
+
+ TopicAdminSettings topicAdminSettings = TopicAdminSettings.newBuilder()
+ .setCredentialsProvider(new NoCredentialsProvider())
+ .setTransportChannelProvider(channelProvider)
+ .build();
+
+ try (TopicAdminClient topicAdminClient = TopicAdminClient.create(topicAdminSettings)) {
+ topicAdminClient.createTopic("projects/demo-test-project/topics/testTopic");
+ }
+
+ // Create a publisher with the channel provider
+ Publisher publisher = Publisher.newBuilder("projects/demo-test-project/topics/testTopic")
+ .setChannelProvider(channelProvider)
+ .setCredentialsProvider(new NoCredentialsProvider())
+ .build();
+
+ // Publish a message to the Pub/Sub emulator
+ PubsubMessage message = PubsubMessage.newBuilder().setData(ByteString.copyFromUtf8("Test message")).build();
+ ApiFuture messageIdFuture = publisher.publish(message);
+ assertNotNull(messageIdFuture.get(), "Expected message to be published successfully");
+
+ // Shutdown the channel
+ channel.shutdownNow();
+ }
+
+ @Test
+ public void testStorageEmulatorConnection() throws IOException {
+ int storagePort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.CLOUD_STORAGE);
+
+ Storage storage = StorageOptions.newBuilder()
+ .setHost("http://" + firebaseContainer.getHost() + ":" + storagePort)
+ .setProjectId("demo-test-project")
+ .setCredentials(NoCredentials.getInstance())
+ .build().getService();
+
+ var bucketName = "demo-test-project.appspot.com";
+
+ BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, "test-upload")
+ .setContentType("application/json")
+ .setContentDisposition("attachment; filename=\"test-upload\"")
+ .build();
+
+ try (var writer = storage.writer(blobInfo)) {
+ writer.write(ByteBuffer.wrap("{\"test\": 1}".getBytes(StandardCharsets.UTF_8)));
+ }
+
+ try (var reader = storage.reader(blobInfo.getBlobId())) {
+ try (var bufReader = new BufferedReader(Channels.newReader(reader, StandardCharsets.UTF_8))) {
+ var contents = bufReader.readLine();
+ assertEquals("{\"test\": 1}", contents, "Expected blob content to match");
+ }
+ }
+ }
+
+ @Test
+ public void testEmulatorUIReachable() throws Exception {
+ // Get the host and port for the Emulator UI
+ int uiPort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.EMULATOR_SUITE_UI);
+
+ // Construct the URL for the Emulator UI root (where index.html would be served)
+ URL url = new URI("http://" + firebaseContainer.getHost() + ":" + uiPort + "/").toURL();
+
+ // Open a connection and send an HTTP GET request
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+
+ // Get the response code to confirm the UI is reachable
+ int responseCode = connection.getResponseCode();
+ assertEquals(200, responseCode, "Expected HTTP status 200 for Emulator UI index.html");
+
+ // Close the connection
+ connection.disconnect();
+ }
+
+ @Test
+ public void testEmulatorHub() throws Exception {
+ // Get the host and port for the Emulator UI
+ int uiPort = firebaseContainer.emulatorPort(FirebaseEmulatorContainer.Emulator.EMULATOR_HUB);
+
+ // Construct the URL for the Emulator UI root (where index.html would be served)
+ URL url = new URI("http://" + firebaseContainer.getHost() + ":" + uiPort + "/emulators").toURL();
+
+ // Open a connection and send an HTTP GET request
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+
+ // Get the response code to confirm the UI is reachable
+ int responseCode = connection.getResponseCode();
+ assertEquals(200, responseCode, "Expected HTTP status 200 for Emulator Hub API");
+
+ // Close the connection
+ connection.disconnect();
+ }
+
+ @Test
+ public void testHosting() throws IOException, InterruptedException, URISyntaxException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ var request = HttpRequest.newBuilder()
+ .GET()
+ .uri(new URI("http://localhost:6006/index.html"))
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ var body = response.body();
+ assertEquals("Hello, Firebase Hosting!
", body);
+ }
+
+ @Test
+ public void testFunctions() throws IOException, InterruptedException, URISyntaxException {
+ HttpClient httpClient = HttpClient.newHttpClient();
+ var request = HttpRequest.newBuilder()
+ .GET()
+ .uri(new URI("http://localhost:6007/demo-test-project/us-central1/helloworld"))
+ .build();
+ var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+ var body = response.body();
+ assertEquals("Hello world", body);
+ }
+
+}
diff --git a/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/TestableFirebaseEmulatorContainer.java b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/TestableFirebaseEmulatorContainer.java
new file mode 100644
index 00000000..f43be657
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/java/io/quarkiverse/googlecloudservices/firebase/deployment/testcontainers/TestableFirebaseEmulatorContainer.java
@@ -0,0 +1,78 @@
+package io.quarkiverse.googlecloudservices.firebase.deployment.testcontainers;
+
+import java.util.function.Consumer;
+
+import com.google.firebase.FirebaseApp;
+import com.google.firebase.FirebaseOptions;
+import com.google.firebase.internal.EmulatorCredentials;
+
+/**
+ * Subclass of {@link FirebaseEmulatorContainer} which has some extra facilities to ease testing. Functionally
+ * this class is equivalent of its superclass with respect to the testing we need to perform.
+ */
+public class TestableFirebaseEmulatorContainer {
+
+ private final String name;
+ private final Consumer options;
+ private FirebaseApp app;
+
+ /**
+ * Creates a new Firebase Emulator container
+ *
+ * @param name The name of the firebase app (must be unique across the JVM).
+ * @param options Consumer to handle additional changes to the FirebaseOptions.Builder.
+ */
+ public TestableFirebaseEmulatorContainer(String name, Consumer options) {
+ this.name = name;
+ this.options = options;
+ }
+
+ /**
+ * Creates a new Firebase Emulator container
+ *
+ * @param name The name of the firebase app (must be unique across the JVM).
+ */
+ public TestableFirebaseEmulatorContainer(String name) {
+ this.name = name;
+ this.options = null;
+ }
+
+ public FirebaseEmulatorContainer.Builder testBuilder() {
+ var builder = FirebaseEmulatorContainer.builder();
+
+ /*
+ * We determine the current group and user using an env variable. This is set by the GitHub Actions runner.
+ * The user and group are used to set the user/group for the user in the docker container run by
+ * TestContainers for the Firebase Emulators. This way, the data exported by the Firebase Emulators
+ * can be read from the build.
+ */
+ builder.withDockerConfig()
+ .withUserIdFromEnv("CURRENT_USER")
+ .withGroupIdFromEnv("CURRENT_GROUP")
+ .afterStart(this::afterStart)
+ .done()
+ .withFirebaseVersion("latest")
+ .withCliArguments()
+ .withProjectId("demo-test-project")
+ .done();
+
+ return builder;
+ }
+
+ private void afterStart(FirebaseEmulatorContainer container) {
+ var firebaseBuilder = FirebaseOptions.builder()
+ .setProjectId("demo-test-project")
+ .setCredentials(new EmulatorCredentials());
+
+ if (options != null) {
+ options.accept(firebaseBuilder);
+ }
+
+ FirebaseOptions options = firebaseBuilder.build();
+ app = FirebaseApp.initializeApp(options, name);
+ }
+
+ public FirebaseApp getApp() {
+ return app;
+ }
+}
diff --git a/firebase-devservices/deployment/src/test/storage.rules b/firebase-devservices/deployment/src/test/storage.rules
new file mode 100644
index 00000000..17f5f58e
--- /dev/null
+++ b/firebase-devservices/deployment/src/test/storage.rules
@@ -0,0 +1,10 @@
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /company/{allPaths=**} {
+ allow read: if true
+ }
+ match /building/{allPaths=**} {
+ allow read: if true
+ }
+ }
+}
diff --git a/firebase-devservices/pom.xml b/firebase-devservices/pom.xml
new file mode 100644
index 00000000..0ad36293
--- /dev/null
+++ b/firebase-devservices/pom.xml
@@ -0,0 +1,20 @@
+
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-services-parent
+ 2.14.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-google-cloud-firebase-devservices-parent
+ Quarkus - Google Cloud Services - Firebas Dev Services
+ pom
+
+
+ runtime
+ deployment
+
+
+
\ No newline at end of file
diff --git a/firebase-devservices/runtime/pom.xml b/firebase-devservices/runtime/pom.xml
new file mode 100644
index 00000000..c9e69e71
--- /dev/null
+++ b/firebase-devservices/runtime/pom.xml
@@ -0,0 +1,55 @@
+
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-firebase-devservices-parent
+ 2.14.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ quarkus-google-cloud-firebase-devservices
+ Quarkus - Google Cloud Services - Firebase Dev Services - Runtime
+ Use Google Cloud Firebase
+
+
+
+ io.quarkiverse.googlecloudservices
+ quarkus-google-cloud-common
+
+
+
+
+
+
+ io.quarkus
+ quarkus-extension-maven-plugin
+ ${quarkus.version}
+
+
+
+ extension-descriptor
+
+ compile
+
+ ${project.groupId}:${project.artifactId}-deployment:${project.version}
+
+
+
+
+
+
+ maven-compiler-plugin
+
+
+
+ io.quarkus
+ quarkus-extension-processor
+ ${quarkus.version}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/firebase-devservices/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/firebase-devservices/runtime/src/main/resources/META-INF/quarkus-extension.yaml
new file mode 100644
index 00000000..c04a5b47
--- /dev/null
+++ b/firebase-devservices/runtime/src/main/resources/META-INF/quarkus-extension.yaml
@@ -0,0 +1,16 @@
+---
+name: "Google Cloud Firebase Devservices"
+artifact: ${project.groupId}:${project.artifactId}:${project.version}
+metadata:
+ keywords:
+ - "firebase"
+ - "google cloud"
+ - "gcloud"
+ - "gcp"
+ categories:
+ - "cloud"
+ - "data"
+ guide: "https://quarkiverse.github.io/quarkiverse-docs/quarkus-google-cloud-services/main/firebase-devservices.html"
+ status: "experimental"
+ config:
+ - "quarkus.google.cloud.devservices."
diff --git a/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirebaseDevServiceConfig.java b/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirebaseDevServiceConfig.java
new file mode 100644
index 00000000..418a634d
--- /dev/null
+++ b/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirebaseDevServiceConfig.java
@@ -0,0 +1,23 @@
+package io.quarkiverse.googlecloudservices.firestore.deployment;
+
+import java.util.Optional;
+
+import io.quarkus.runtime.annotations.ConfigRoot;
+import io.smallrye.config.ConfigMapping;
+
+/**
+ * Config mapping to detect if the Firebase Dev Services are running, in which case the PubSub dev service
+ * will be disabled by default as these two Devservice are in conflict with each other.
+ */
+@ConfigMapping(prefix = "quarkus.google.cloud.firebase.devservice")
+@ConfigRoot
+public interface FirebaseDevServiceConfig {
+
+ /**
+ * Indicates to use the dev service for Firebase. The default value is not setup unless the firebase module
+ * is included. In that case, the Firebase devservices will by default be preferred and the DevService for
+ * PubSub will be disabled.
+ */
+ Optional preferFirebaseDevServices();
+
+}
diff --git a/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirestoreDevServiceProcessor.java b/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirestoreDevServiceProcessor.java
index aebcab1f..86cd3f11 100644
--- a/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirestoreDevServiceProcessor.java
+++ b/firestore/deployment/src/main/java/io/quarkiverse/googlecloudservices/firestore/deployment/FirestoreDevServiceProcessor.java
@@ -35,6 +35,7 @@ public class FirestoreDevServiceProcessor {
@BuildStep
public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildItem,
FirestoreBuildTimeConfig buildTimeConfig,
+ FirebaseDevServiceConfig firebaseConfig,
List devServicesSharedNetworkBuildItem,
Optional consoleInstalledBuildItem,
CuratedApplicationShutdownBuildItem closeBuildItem,
@@ -56,7 +57,7 @@ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildI
// Try starting the container if conditions are met
try {
- devService = startContainerIfAvailable(dockerStatusBuildItem, buildTimeConfig.devservice(),
+ devService = startContainerIfAvailable(dockerStatusBuildItem, buildTimeConfig.devservice(), firebaseConfig,
globalDevServicesConfig.timeout);
} catch (Throwable t) {
LOGGER.warn("Unable to start Firestore dev service", t);
@@ -80,6 +81,7 @@ public DevServicesResultBuildItem start(DockerStatusBuildItem dockerStatusBuildI
*/
private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(DockerStatusBuildItem dockerStatusBuildItem,
FirestoreDevServiceConfig config,
+ FirebaseDevServiceConfig firebaseConfig,
Optional timeout) {
if (!config.enabled()) {
// Firestore service explicitly disabled
@@ -87,6 +89,12 @@ private DevServicesResultBuildItem.RunningDevService startContainerIfAvailable(D
return null;
}
+ if (firebaseConfig.preferFirebaseDevServices().orElse(false)) {
+ // Firebase DevServices are included, use them instead
+ LOGGER.debug("Not starting Dev Services for Firestore as the Firebase DevServices are preferred");
+ return null;
+ }
+
if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) {
LOGGER.warn("Not starting devservice because docker is not available");
return null;
diff --git a/integration-tests/firebase-admin/firebase.json b/integration-tests/firebase-admin/firebase.json
index 2fb2a16b..94fd7894 100644
--- a/integration-tests/firebase-admin/firebase.json
+++ b/integration-tests/firebase-admin/firebase.json
@@ -1,10 +1,21 @@
{
"emulators": {
"auth": {
- "port": 9099
+ "port": 9099,
+ "host": "0.0.0.0"
},
"ui": {
- "enabled": true
+ "port": 4000,
+ "enabled": true,
+ "host": "0.0.0.0"
+ },
+ "hub": {
+ "port": 4400,
+ "host": "0.0.0.0"
+ },
+ "logging": {
+ "port": 4500,
+ "host": "0.0.0.0"
},
"singleProjectMode": true
}
diff --git a/integration-tests/firebase-admin/pom.xml b/integration-tests/firebase-admin/pom.xml
index 15eb82b5..dac3740a 100644
--- a/integration-tests/firebase-admin/pom.xml
+++ b/integration-tests/firebase-admin/pom.xml
@@ -37,6 +37,10 @@
rest-assured
test